Skip to content

Commit

Permalink
feat: add normalizedNumberFormat for stack: "normalize" tooltips (#…
Browse files Browse the repository at this point in the history
…8307)

Co-authored-by: GitHub Actions Bot <vega-actions-bot@users.noreply.github.com>
Co-authored-by: Kanit Wongsuphasawat <kanitw@gmail.com>
  • Loading branch information
3 people committed Jul 22, 2022
1 parent 94dfdb9 commit 19deddc
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 93 deletions.
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 (
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")');
});
});
});

0 comments on commit 19deddc

Please sign in to comment.