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 all 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:__ `%`",
"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\"], \".0%\")) + \"; series: \" + (isValid(datum[\"series\"]) ? datum[\"series\"] : \"\"+datum[\"series\"])"
},
"x": {"scale": "x", "field": "yearmonth_date"},
"y": {"scale": "y", "field": "sum_count_end"},
Expand Down
Binary file modified examples/compiled/stacked_bar_h_normalized_labeled.png
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_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.
5 changes: 3 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\"], \".0%\")) + \"; 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\"], \".0%\")) + \"; 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 Expand Up @@ -154,6 +154,7 @@
"orient": "bottom",
"grid": false,
"title": "population",
"format": ".0%",
"labelFlush": true,
"labelOverlap": true,
"tickCount": {"signal": "ceil(width/40)"},
Expand Down
Binary file modified examples/compiled/stacked_bar_normalize.png
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.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 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\"], \".0%\")) + \"; gender: \" + (isValid(datum[\"gender\"]) ? datum[\"gender\"] : \"\"+datum[\"gender\"])"
},
"x": {"scale": "x", "field": "age"},
"width": {"scale": "x", "band": 1},
Expand Down Expand Up @@ -120,6 +120,7 @@
"orient": "left",
"grid": false,
"title": "population",
"format": ".0%",
"labelOverlap": true,
"tickCount": {"signal": "ceil(height/40)"},
"zindex": 0
Expand Down
45 changes: 30 additions & 15 deletions 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 All @@ -25,21 +25,36 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
} else if (
lsh marked this conversation as resolved.
Show resolved Hide resolved
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.numberFormatType &&
channelDefType(fieldOrDatumDef) === 'quantitative'
channelDefType(fieldOrDatumDef) === 'quantitative' &&
config.customFormatTypes
) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
config
}),
...specifiedLabelsSpec
};
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
};
}
}

return specifiedLabelsSpec;
}
67 changes: 60 additions & 7 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 Down Expand Up @@ -85,7 +103,7 @@ export function formatSignalRef({
return signal ? {signal} : undefined;
}

format = numberFormat(type, format, config);
format = numberFormat({type, specifiedFormat: format, config, normalizeStack});
if (isFieldDef(fieldOrDatumDef) && isBinning(fieldOrDatumDef.bin)) {
const endField = vgField(fieldOrDatumDef, {expr, binSuffix: 'end'});
return {
Expand Down Expand Up @@ -161,8 +179,34 @@ export function guideFormat(
) {
if (isCustomFormatType(formatType)) {
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.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
}
}

if (
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize' &&
config.normalizedNumberFormat
) {
return numberFormat({
type: 'quantitative',
config,
normalizeStack: true
});
}

if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
Expand All @@ -171,7 +215,7 @@ export function guideFormat(
return timeFormat(format as string, timeUnit, config, omitTimeFormatConfig);
}

return numberFormat(type, format, config);
return numberFormat({type, specifiedFormat: format, config});
}

export function guideFormatType(
Expand All @@ -191,15 +235,25 @@ export function guideFormatType(
/**
* Returns number format for a fieldDef.
*/
export function numberFormat(type: Type, specifiedFormat: string | Dict<unknown>, config: Config) {
export function numberFormat({
type,
specifiedFormat,
config,
normalizeStack
}: {
type: Type;
specifiedFormat?: string | Dict<unknown>;
config: Config;
normalizeStack?: 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 normalizeStack ? config.normalizedNumberFormat : config.numberFormat;
}
return undefined;
}
Expand Down Expand Up @@ -243,7 +297,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:__ `%`
*/
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: '.0%'
};

// 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")');
});
});
});