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 normalizedNumberFormat for stack: "normalize" tooltips #8307

Merged
merged 23 commits into from Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion build/vega-lite-schema.json
Expand Up @@ -7643,8 +7643,16 @@
"$ref": "#/definitions/MarkConfig",
"description": "Mark Config"
},
"normalizedNumberFormat": {
"description": "If normalizedNumberFormatType is not specified, D3 number format for axis labels, text marks, and tooltips of normalized stacked fields (fields with `stack: \"normalize\"`). For example `\"s\"` for SI units. Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).\n\nIf `config.normalizedNumberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function. __Default value:__ `.2%`",
"type": "string"
},
"normalizedNumberFormatType": {
"description": "[Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type) for `config.normalizedNumberFormat`.\n\n__Default value:__ `undefined` -- This is equilvalent to call D3-format, which is exposed as [`format` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#format). __Note:__ You must also set `customFormatTypes` to `true` to use this feature.",
"type": "string"
},
"numberFormat": {
"description": "If numberFormatType is not specified, D3 Number format for guide labels and text marks. For example `\"s\"` for SI units. Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).\n\nIf `config.numberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function.",
"description": "If numberFormatType is not specified, D3 number format for guide labels, text marks, and tooltips of non-normalized fields (fields *without* `stack: \"normalize\"`). For example `\"s\"` for SI units. Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).\n\nIf `config.numberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function.",
"type": "string"
},
"numberFormatType": {
Expand Down
2 changes: 1 addition & 1 deletion examples/compiled/stacked_area_normalize.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/compiled/stacked_area_normalize.vg.json
Expand Up @@ -72,7 +72,7 @@
"orient": {"value": "vertical"},
"fill": {"scale": "color", "field": "series"},
"description": {
"signal": "\"date (year-month): \" + (timeFormat(datum[\"yearmonth_date\"], '%Y')) + \"; Sum of count: \" + (format(datum[\"sum_count_end\"]-datum[\"sum_count_start\"], \"\")) + \"; series: \" + (isValid(datum[\"series\"]) ? datum[\"series\"] : \"\"+datum[\"series\"])"
"signal": "\"date (year-month): \" + (timeFormat(datum[\"yearmonth_date\"], '%Y')) + \"; Sum of count: \" + (format(datum[\"sum_count_end\"]-datum[\"sum_count_start\"], \".2%\")) + \"; series: \" + (isValid(datum[\"series\"]) ? datum[\"series\"] : \"\"+datum[\"series\"])"
},
"x": {"scale": "x", "field": "yearmonth_date"},
"y": {"scale": "y", "field": "sum_count_end"},
Expand Down
2 changes: 1 addition & 1 deletion examples/compiled/stacked_bar_h_normalized_labeled.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions examples/compiled/stacked_bar_h_normalized_labeled.vg.json
Expand Up @@ -72,7 +72,7 @@
"fill": {"scale": "color", "field": "gender"},
"ariaRoleDescription": {"value": "bar"},
"description": {
"signal": "\"population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \"\")) + \"; age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
"signal": "\"population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \".2%\")) + \"; age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
},
"x": {"scale": "x", "field": "sum_people_end"},
"x2": {"scale": "x", "field": "sum_people_start"},
Expand All @@ -91,7 +91,7 @@
"opacity": {"value": 0.9},
"fill": {"value": "white"},
"description": {
"signal": "\"population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \"\")) + \"; age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
"signal": "\"population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \".2%\")) + \"; age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
},
"x": {
"signal": "scale(\"x\", 0.5 * datum[\"sum_people_start\"] + 0.5 * datum[\"sum_people_end\"])"
Expand Down
2 changes: 1 addition & 1 deletion examples/compiled/stacked_bar_normalize.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/compiled/stacked_bar_normalize.vg.json
Expand Up @@ -56,7 +56,7 @@
"fill": {"scale": "color", "field": "gender"},
"ariaRoleDescription": {"value": "bar"},
"description": {
"signal": "\"age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \"\")) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
"signal": "\"age: \" + (isValid(datum[\"age\"]) ? datum[\"age\"] : \"\"+datum[\"age\"]) + \"; population: \" + (format(datum[\"sum_people_end\"]-datum[\"sum_people_start\"], \".2%\")) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
},
"x": {"scale": "x", "field": "age"},
"width": {"scale": "x", "band": 1},
Expand Down
21 changes: 20 additions & 1 deletion src/compile/axis/encode.ts
@@ -1,5 +1,5 @@
import {getSecondaryRangeChannel, PositionScaleChannel} from '../../channel';
import {channelDefType, getFieldOrDatumDef} from '../../channeldef';
import {channelDefType, getFieldOrDatumDef, isPositionFieldOrDatumDef} from '../../channeldef';
import {formatCustomType, isCustomFormatType} from '../format';
import {UnitModel} from '../unit';

Expand Down Expand Up @@ -39,6 +39,25 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
}),
...specifiedLabelsSpec
};
} else if (
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.normalizedNumberFormatType &&
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize' &&
channelDefType(fieldOrDatumDef) === 'quantitative'
) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
config
}),
...specifiedLabelsSpec
};
}

return specifiedLabelsSpec;
Expand Down
56 changes: 52 additions & 4 deletions src/compile/format.ts
Expand Up @@ -7,6 +7,7 @@ import {
FieldDef,
isFieldDef,
isFieldOrDatumDefForTimeFormat,
isPositionFieldOrDatumDef,
isScaleFieldDef,
vgField
} from '../channeldef';
Expand Down Expand Up @@ -58,6 +59,23 @@ 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 &&
Expand All @@ -74,6 +92,23 @@ export function formatSignalRef({
});
}

if (
lsh marked this conversation as resolved.
Show resolved Hide resolved
normalizeStack &&
type === 'quantitative' &&
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.normalizedNumberFormatType
) {
return formatCustomType({
fieldOrDatumDef,
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
expr,
config
});
}

if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
const signal = timeFormatExpression(
field,
Expand All @@ -85,7 +120,7 @@ export function formatSignalRef({
return signal ? {signal} : undefined;
}

format = numberFormat(type, format, config);
format = numberFormat(type, format, config, normalizeStack);
if (isFieldDef(fieldOrDatumDef) && isBinning(fieldOrDatumDef.bin)) {
const endField = vgField(fieldOrDatumDef, {expr, binSuffix: 'end'});
return {
Expand Down Expand Up @@ -163,6 +198,15 @@ export function guideFormat(
return undefined; // handled in encode block
} else if (format === undefined && formatType === undefined && config.numberFormatType && config.customFormatTypes) {
return undefined; // handled in encode block
} else if (
format === undefined &&
formatType === undefined &&
config.normalizedNumberFormatType &&
config.customFormatTypes &&
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize'
) {
return undefined; // handled in encode block
}

if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
Expand Down Expand Up @@ -191,15 +235,20 @@ export function guideFormatType(
/**
* Returns number format for a fieldDef.
*/
export function numberFormat(type: Type, specifiedFormat: string | Dict<unknown>, config: Config) {
export function numberFormat(
type: Type,
specifiedFormat: string | Dict<unknown>,
config: Config,
normalizedStack?: boolean
) {
// Specified format in axis/legend has higher precedence than fieldDef.format
if (isString(specifiedFormat)) {
return specifiedFormat;
}

if (type === QUANTITATIVE) {
// we only apply the default if the field is quantitative
return config.numberFormat;
return normalizedStack ? config.normalizedNumberFormat : config.numberFormat;
}
return undefined;
}
Expand Down Expand Up @@ -243,7 +292,6 @@ export function binFormatExpression(
if (format === undefined && formatType === undefined && config.customFormatTypes && config.numberFormatType) {
return binFormatExpression(startField, endField, config.numberFormat, 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}`;
Expand Down
27 changes: 25 additions & 2 deletions src/config.ts
Expand Up @@ -146,7 +146,7 @@ export interface VLOnlyConfig<ES extends ExprRef | SignalRef> {

/**
* If numberFormatType is not specified,
* D3 Number format for guide labels and text marks. For example `"s"` for SI units.
* D3 number format for guide labels, text marks, and tooltips of non-normalized fields (fields *without* `stack: "normalize"`). For example `"s"` for SI units.
* Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).
*
* If `config.numberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function.
Expand All @@ -162,6 +162,25 @@ export interface VLOnlyConfig<ES extends ExprRef | SignalRef> {
*/
numberFormatType?: string;

/**
* If normalizedNumberFormatType is not specified,
* D3 number format for axis labels, text marks, and tooltips of normalized stacked fields (fields with `stack: "normalize"`). For example `"s"` for SI units.
* Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).
*
* If `config.normalizedNumberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function.
* __Default value:__ `.2%`
*/
normalizedNumberFormat?: string;

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

/**
* Default time format for raw time values (without time units) in text marks, legend labels and header labels.
*
Expand Down Expand Up @@ -330,7 +349,9 @@ export const defaultConfig: Config<SignalRef> = {
title: {},

facet: {spacing: DEFAULT_SPACING},
concat: {spacing: DEFAULT_SPACING}
concat: {spacing: DEFAULT_SPACING},

normalizedNumberFormat: '.2%'
};

// Tableau10 color palette, copied from `vegaScale.scheme('tableau10')`
Expand Down Expand Up @@ -593,6 +614,8 @@ const VL_ONLY_CONFIG_PROPERTIES: (keyof Config)[] = [
'concat',
'numberFormat',
'numberFormatType',
'normalizedNumberFormat',
'normalizedNumberFormatType',
'timeFormat',
'countTitle',
'header',
Expand Down
16 changes: 16 additions & 0 deletions test/compile/axis/encode.test.ts
Expand Up @@ -61,5 +61,21 @@ describe('compile/axis/encode', () => {
const labels = encode.labels(model, 'x', {});
expect(labels.text.signal).toBe('customNumberFormat(datum.value, "abc")');
});

it('applies custom format type from a normalized stack', () => {
const model = parseUnitModelWithScale({
mark: 'point',
encoding: {
x: {field: 'a', type: 'quantitative', stack: 'normalize'}
},
config: {
customFormatTypes: true,
normalizedNumberFormat: 'abc',
normalizedNumberFormatType: 'customNumberFormat'
}
});
const labels = encode.labels(model, 'x', {});
expect(labels.text.signal).toBe('customNumberFormat(datum.value, "abc")');
});
});
});
78 changes: 77 additions & 1 deletion test/compile/format.test.ts
@@ -1,4 +1,4 @@
import {vgField} from '../../src/channeldef';
import {PositionDatumDef, vgField} from '../../src/channeldef';
import {
formatSignalRef,
guideFormat,
Expand Down Expand Up @@ -82,6 +82,10 @@ describe('Format', () => {
expect(numberFormat(QUANTITATIVE, undefined, {numberFormat: 'd'})).toBe('d');
});

it('should use normalized number format for quantitative scale with stack: "normalize"', () => {
expect(numberFormat(QUANTITATIVE, undefined, {numberFormat: 'd', normalizedNumberFormat: 'c'}, true)).toBe('c');
lsh marked this conversation as resolved.
Show resolved Hide resolved
});

it('should use number format for ordinal and nominal data but don not use config', () => {
for (const type of [ORDINAL, NOMINAL]) {
expect(numberFormat(type, undefined, {numberFormat: 'd'})).toBeUndefined();
Expand Down Expand Up @@ -178,6 +182,66 @@ describe('Format', () => {
signal: 'customFormatter(200, "abc")'
});
});

it('should use a custom formatter datumDef if config.normalizedNumberFormatType is present and stack is normalized', () => {
expect(
formatSignalRef({
fieldOrDatumDef: {datum: 200, type: 'quantitative'},
format: undefined,
formatType: undefined,
expr: 'parent',
normalizeStack: true,
config: {
normalizedNumberFormat: 'abc',
normalizedNumberFormatType: 'customFormatter',
customFormatTypes: true
}
})
).toEqual({
signal: 'customFormatter(200, "abc")'
});
});

it('should prefer normalizedNumberFormat over numberFormat when stack is normalized', () => {
lsh marked this conversation as resolved.
Show resolved Hide resolved
expect(
formatSignalRef({
fieldOrDatumDef: {datum: 200, type: 'quantitative'},
format: undefined,
formatType: undefined,
expr: 'parent',
normalizeStack: true,
config: {
numberFormat: 'def',
numberFormatType: 'customFormatter2',
normalizedNumberFormat: 'abc',
normalizedNumberFormatType: 'customFormatter',
customFormatTypes: true
}
})
).toEqual({
signal: 'customFormatter(200, "abc")'
});
});

it('should prefer numberFormat over normalizedNumberFormat when stack is not normalized', () => {
expect(
formatSignalRef({
fieldOrDatumDef: {datum: 200, type: 'quantitative'},
format: undefined,
formatType: undefined,
expr: 'parent',
config: {
numberFormat: 'def',
numberFormatType: 'customFormatter2',
normalizedNumberFormat: 'abc',
normalizedNumberFormatType: 'customFormatter',
customFormatTypes: true
}
})
).toEqual({
signal: 'customFormatter2(200, "def")'
});
});
});

describe('guideFormat', () => {
Expand All @@ -196,6 +260,18 @@ describe('Format', () => {
);
expect(format).toBeUndefined();
});

it('returns undefined for custom normalizedNumberFormatType in the config', () => {
const format = guideFormat(
{datum: 200, type: 'quantitative', stack: 'normalize'} as PositionDatumDef<string>,
'quantitative',
undefined,
undefined,
{normalizedNumberFormat: 'abc', normalizedNumberFormatType: 'customFormatter', customFormatTypes: true},
false
);
expect(format).toBeUndefined();
});
});

describe('guideFormatType()', () => {
Expand Down