Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Axis/Axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Axis: FC<AxisProps> = ({
labelFormat,
labelOrientation = DEFAULT_LABEL_ORIENTATION,
labels,
numberFormat,
numberFormat = 'shortNumber',
range = undefined,
subLabels,
ticks = false,
Expand Down
46 changes: 25 additions & 21 deletions src/specBuilder/axis/axisLabelUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getLabelAnchorValues,
getLabelAngle,
getLabelFormat,
getLabelNumberFormat,
getLabelOffset,
getLabelValue,
labelIsParallelToAxis,
Expand Down Expand Up @@ -157,29 +158,13 @@ describe('getLabelAngle', () => {
});

describe('getLabelFormat()', () => {
test('should include the number format test if numberFormat exists', () => {
const labelFormat = getLabelFormat(
{ ...defaultAxisProps, labelFormat: 'linear', numberFormat: '.2f' },
'xLinear'
);
expect(labelFormat).toHaveLength(4);
expect(labelFormat[0]).toEqual({ test: 'isNumber(datum.value)', signal: "format(datum.value, '.2f')" });
});
test('should not include the number format test if numberFormat does not exist or is an empty string', () => {
expect(
getLabelFormat({ ...defaultAxisProps, labelFormat: 'linear', numberFormat: undefined }, 'xLinear')
).toHaveLength(3);
expect(
getLabelFormat({ ...defaultAxisProps, labelFormat: 'linear', numberFormat: '' }, 'xLinear')
).toHaveLength(3);
});
test('should include text truncation if truncateText is true', () => {
const labelEncodings = getLabelFormat({ ...defaultAxisProps, truncateLabels: true }, 'xBand');
expect(labelEncodings).toHaveLength(3);
expect(labelEncodings[2].signal).toContain('truncateText');
expect(labelEncodings).toHaveLength(2);
expect(labelEncodings[1].signal).toContain('truncateText');
});
test('should not include text truncation if the scale name does not include band', () => {
expect(getLabelFormat({ ...defaultAxisProps, truncateLabels: true }, 'xLinear')[2].signal).not.toContain(
expect(getLabelFormat({ ...defaultAxisProps, truncateLabels: true }, 'xLinear')[1].signal).not.toContain(
'truncateText'
);
});
Expand All @@ -188,13 +173,13 @@ describe('getLabelFormat()', () => {
getLabelFormat(
{ ...defaultAxisProps, truncateLabels: true, position: 'bottom', labelOrientation: 'vertical' },
'xBand'
)[2].signal
)[1].signal
).not.toContain('truncateText');
expect(
getLabelFormat(
{ ...defaultAxisProps, truncateLabels: true, position: 'left', labelOrientation: 'horizontal' },
'yBand'
)[2].signal
)[1].signal
).not.toContain('truncateText');
});
test('should return duration formatter if labelFormat is duration', () => {
Expand All @@ -204,3 +189,22 @@ describe('getLabelFormat()', () => {
);
});
});

describe('getLabelNumberFormat()', () => {
test('should return correct signal for shortNumber', () => {
expect(getLabelNumberFormat('shortNumber')).toHaveProperty(
'signal',
"upper(replace(format(datum.value, '.3~s'), /(\\d+)G/, '$1B'))"
);
});
test('should return correct signal for shortCurrency', () => {
expect(getLabelNumberFormat('shortCurrency')).toHaveProperty(
'signal',
"upper(replace(format(datum.value, '$.3~s'), /(\\d+)G/, '$1B'))"
);
});
test('should return correct signal for string specifier', () => {
const numberFormat = '.2f';
expect(getLabelNumberFormat(numberFormat)).toHaveProperty('signal', `format(datum.value, '${numberFormat}')`);
});
});
41 changes: 30 additions & 11 deletions src/specBuilder/axis/axisLabelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { AxisSpecProps, Granularity, Label, LabelAlign, Orientation, Position } from 'types';
import { getD3FormatSpecifierFromNumberFormat } from '@specBuilder/specUtils';
import { AxisSpecProps, Granularity, Label, LabelAlign, NumberFormat, Orientation, Position } from 'types';
import {
Align,
Baseline,
Expand Down Expand Up @@ -241,23 +242,41 @@ export const getLabelFormat = (
return { signal: 'formatTimeDurationLabels(datum)' };
}

// if it's a number and greater than or equal to 1000, we want to format it in scientific notation (but with B instead of G) ex. 1K, 20M, 1.3B
return [
...(numberFormat ? [{ test: 'isNumber(datum.value)', signal: `format(datum.value, '${numberFormat}')` }] : []),
{
test: 'isNumber(datum.value) && abs(datum.value) >= 1000',
signal: "upper(replace(format(datum.value, '.3~s'), 'G', 'B'))",
},
{
test: 'isNumber(datum.value)',
signal: 'format(datum.value, ",")',
},
getLabelNumberFormat(numberFormat),
...(truncateLabels && scaleName.includes('Band') && labelIsParallelToAxis(position, labelOrientation)
? [{ signal: 'truncateText(datum.value, bandwidth("xBand")/(1- paddingInner), "normal", 14)' }]
: [{ signal: 'datum.value' }]),
];
};

/**
* gets the number format tests and signals based on the numberFormat
* @param numberFormat
* @returns
*/
export const getLabelNumberFormat = (
numberFormat: NumberFormat | string
): {
test?: string;
} & TextValueRef => {
const defaultTest = 'isNumber(datum.value)';
if (numberFormat === 'shortNumber') {
return {
test: `${defaultTest} && abs(datum.value) >= 1000`,
signal: "upper(replace(format(datum.value, '.3~s'), /(\\d+)G/, '$1B'))",
};
}
if (numberFormat === 'shortCurrency') {
return {
test: `${defaultTest} && abs(datum.value) >= 1000`,
signal: "upper(replace(format(datum.value, '$.3~s'), /(\\d+)G/, '$1B'))",
};
}
const d3FormatSpecifier = getD3FormatSpecifierFromNumberFormat(numberFormat);
return { test: defaultTest, signal: `format(datum.value, '${d3FormatSpecifier}')` };
};

/**
* Gets the axis label encoding
* @param labelAlign
Expand Down
1 change: 1 addition & 0 deletions src/specBuilder/axis/axisReferenceLineUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const defaultAxisProps: AxisSpecProps = {
labelFontWeight: DEFAULT_LABEL_FONT_WEIGHT,
labelOrientation: DEFAULT_LABEL_ORIENTATION,
labels: [],
numberFormat: 'shortNumber',
position: 'bottom',
scaleType: 'linear',
subLabels: [],
Expand Down
12 changes: 4 additions & 8 deletions src/specBuilder/axis/axisSpecBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,7 @@ const defaultAxis: Axis = {
text: [
{
test: 'isNumber(datum.value) && abs(datum.value) >= 1000',
signal: "upper(replace(format(datum.value, '.3~s'), 'G', 'B'))",
},
{
test: 'isNumber(datum.value)',
signal: 'format(datum.value, ",")',
signal: "upper(replace(format(datum.value, '.3~s'), /(\\d+)G/, '$1B'))",
},
{ signal: 'datum.value' },
],
Expand Down Expand Up @@ -279,7 +275,7 @@ describe('Spec builder, Axis', () => {
scaleName: 'xLinear',
scaleType: 'linear',
})[0].encode?.labels?.update?.text as ProductionRule<TextValueRef>;
expect(labelTextEncoding).toHaveLength(4);
expect(labelTextEncoding).toHaveLength(3);
expect(labelTextEncoding[0]).toEqual({
test: "abs(scale('xLinear', 10) - scale('xLinear', datum.value)) < 30",
value: '',
Expand All @@ -296,8 +292,8 @@ describe('Spec builder, Axis', () => {
scaleType: 'linear',
})[0].encode?.labels?.update?.text as ProductionRule<TextValueRef>;

// 2 tests for the two reference lines plus 3 default tests = 5 tests
expect(labelTextEncoding).toHaveLength(5);
// 2 tests for the two reference lines plus 2 default tests = 4 tests
expect(labelTextEncoding).toHaveLength(4);
});
test('should set the values on the axis if labels is set', () => {
const axes = addAxes([], {
Expand Down
4 changes: 3 additions & 1 deletion src/specBuilder/axis/axisSpecBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const addAxis = produce<Spec, [AxisProps & { colorScheme?: ColorScheme; i
labelFontWeight = DEFAULT_LABEL_FONT_WEIGHT,
labelOrientation = DEFAULT_LABEL_ORIENTATION,
labels = [],
numberFormat = 'shortNumber',
position,
range,
subLabels = [],
Expand Down Expand Up @@ -90,8 +91,9 @@ export const addAxis = produce<Spec, [AxisProps & { colorScheme?: ColorScheme; i
labelFontWeight,
labelOrientation,
labels,
position,
name: `axis${index}`,
numberFormat,
position,
range,
subLabels,
ticks,
Expand Down
1 change: 1 addition & 0 deletions src/specBuilder/axis/axisTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const defaultAxisProps: AxisSpecProps = {
labelFontWeight: DEFAULT_LABEL_FONT_WEIGHT,
labelOrientation: DEFAULT_LABEL_ORIENTATION,
labels: [],
numberFormat: 'shortNumber',
position: 'bottom',
scaleType: 'linear',
subLabels: [],
Expand Down
14 changes: 4 additions & 10 deletions src/specBuilder/axis/axisUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('getDefaultAxis()', () => {
labelOrientation: 'horizontal',
labels: [],
name: 'axis0',
numberFormat: 'shortNumber',
position: 'left',
scaleType: 'linear',
subLabels: [],
Expand Down Expand Up @@ -81,11 +82,7 @@ describe('getDefaultAxis()', () => {
text: [
{
test: 'isNumber(datum.value) && abs(datum.value) >= 1000',
signal: "upper(replace(format(datum.value, '.3~s'), 'G', 'B'))",
},
{
test: 'isNumber(datum.value)',
signal: 'format(datum.value, ",")',
signal: "upper(replace(format(datum.value, '.3~s'), /(\\d+)G/, '$1B'))",
},
{
signal: 'datum.value',
Expand All @@ -112,6 +109,7 @@ describe('getDefaultAxis()', () => {
labelOrientation: 'horizontal',
labels: [],
name: 'axis0',
numberFormat: 'shortNumber',
position: 'left',
scaleType: 'point',
subLabels: [],
Expand Down Expand Up @@ -144,11 +142,7 @@ describe('getDefaultAxis()', () => {
text: [
{
test: 'isNumber(datum.value) && abs(datum.value) >= 1000',
signal: "upper(replace(format(datum.value, '.3~s'), 'G', 'B'))",
},
{
test: 'isNumber(datum.value)',
signal: 'format(datum.value, ",")',
signal: "upper(replace(format(datum.value, '.3~s'), /(\\d+)G/, '$1B'))",
},
{
signal: 'datum.value',
Expand Down
9 changes: 9 additions & 0 deletions src/specBuilder/specUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { BandScale, OrdinalScale } from 'vega';

import {
getColorValue,
getD3FormatSpecifierFromNumberFormat,
getDimensionField,
getFacetsFromProps,
getFacetsFromScales,
Expand Down Expand Up @@ -195,4 +196,12 @@ describe('specUtils', () => {
);
});
});

describe('getD3FormatSpecifierFromNumberFormat()', () => {
test('should return proper formats', () => {
expect(getD3FormatSpecifierFromNumberFormat('currency')).toEqual('$,.2f');
expect(getD3FormatSpecifierFromNumberFormat('standardNumber')).toEqual(',');
expect(getD3FormatSpecifierFromNumberFormat(',.2f')).toEqual(',.2f');
});
});
});
18 changes: 18 additions & 0 deletions src/specBuilder/specUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
LineType,
LineTypeFacet,
LineWidth,
NumberFormat,
OpacityFacet,
SpectrumColor,
SymbolSize,
Expand Down Expand Up @@ -294,3 +295,20 @@ export const mergeValuesIntoData = (data, values) => {
export const getDimensionField = (dimension: string, scaleType?: ScaleType) => {
return scaleType === 'time' ? DEFAULT_TRANSFORMED_TIME_DIMENSION : dimension;
};

/**
* Gets the d3 format specifier for named number formats.
* shortNumber and shortCurrency are not included since these require additional logic
* @param numberFormat
* @returns
*/
export const getD3FormatSpecifierFromNumberFormat = (numberFormat: NumberFormat | string): string => {
switch (numberFormat) {
case 'currency':
return '$,.2f'; // currency format
case 'standardNumber':
return ','; // standard number format
default:
return numberFormat;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,6 @@ describe('getTrendlineAnnotationTextMark()', () => {
const textMark = getTrendlineAnnotationTextMark({ ...defaultAnnotationProps, prefix: '' });
expect(textMark.encode?.enter?.text).toHaveProperty('signal', `format(datum.datum.${TRENDLINE_VALUE}, '')`);
});
test('should increase offset if badge is true', () => {
const textMark = getTrendlineAnnotationTextMark({ ...defaultAnnotationProps, badge: true });
expect(textMark.transform?.[0]).toHaveProperty('offset', [4, 4, 4, 4, 5.65, 5.65, 5.65, 5.65]);
});
});

describe('getTextFill()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,8 @@ export const getTrendlineAnnotationPointY = ({
* @returns TextMark
*/
export const getTrendlineAnnotationTextMark = (annotation: TrendlineAnnotationSpecProps): TextMark => {
const { badge, name, numberFormat, prefix, trendlineName, markName } = annotation;
const { name, numberFormat, prefix, trendlineName, markName } = annotation;
const textPrefix = prefix ? `'${prefix} ' + ` : '';
// need more offset if there is a badge to make room for said badge
const offset = badge ? [4, 4, 4, 4, 5.65, 5.65, 5.65, 5.65] : [2, 2, 2, 2, 2.83, 2.83, 2.83, 2.83];
const fill = getTextFill({ ...annotation });
return {
name,
Expand All @@ -212,7 +210,7 @@ export const getTrendlineAnnotationTextMark = (annotation: TrendlineAnnotationSp
type: 'label',
size: { signal: '[width, height]' },
avoidMarks: [trendlineName, `${markName}_group`],
offset,
offset: [6, 6, 6, 6, 8.49, 8.49, 8.49, 8.49],
anchor: ['top', 'bottom', 'right', 'left', 'top-right', 'top-left', 'bottom-right', 'bottom-left'],
},
],
Expand Down
17 changes: 16 additions & 1 deletion src/stories/components/Axis/Axis.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ import timeData from './timeData.json';
export default {
title: 'RSC/Axis',
component: Axis,
argTypes: {
lineType: {
control: 'select',
options: ['solid', 'dashed', 'dotted', 'dotDash', 'shortDash', 'longDash', 'twoDash'],
},
lineWidth: {
control: 'inline-radio',
options: ['XS', 'S', 'M', 'L', 'XL'],
},
numberFormat: {
control: 'select',
options: ['currency', 'shortCurrency', 'shortNumber', 'standardNumber', '$,.2f', ',.2%', '.3s'],
},
},
};

const data = [
Expand Down Expand Up @@ -191,13 +205,14 @@ NonLinearAxis.args = {

const NumberFormat = bindWithProps(AxisStory);
NumberFormat.args = {
numberFormat: '$,.2f',
numberFormat: 'shortCurrency',
position: 'left',
baseline: true,
grid: true,
labelFormat: 'linear',
ticks: true,
title: 'Price',
range: [0, 2000000],
};

const CustomXRange = bindWithProps(LinearAxisStory);
Expand Down
Loading