Skip to content

Commit ba0e455

Browse files
authored
Merge pull request #649 from adobe/clamoureux/barAnnotationPadding
feat: added bar annotation padding prop
2 parents b87adf3 + 2e4d3d9 commit ba0e455

File tree

5 files changed

+66
-38
lines changed

5 files changed

+66
-38
lines changed

packages/docs/docs/api/visualizations/Bar.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ The `Annotation` component is used to display a text annotation. The annotation
260260
```jsx
261261
<Chart data={data}>
262262
<Bar {...props}>
263-
<Annotation textKey="textKey" style={{ width: 48 }} />
263+
<Annotation textKey="textKey" />
264264
</Bar>
265265
</Chart>
266266
```
@@ -286,11 +286,17 @@ The `Annotation` component is used to display a text annotation. The annotation
286286
<td>–</td>
287287
<td>The key on each value in the data passed to the chart that contains the text to display in the annotation.</td>
288288
</tr>
289+
<tr>
290+
<td>padding</td>
291+
<td>number</td>
292+
<td>4</td>
293+
<td>Padding in pixels around the annotation text (applies to both horizontal and vertical padding).</td>
294+
</tr>
289295
<tr>
290296
<td>style</td>
291297
<td>\{width: number}</td>
292298
<td>-</td>
293-
<td>Style overrides. Width is used in place of dynamically calculated width.</td>
299+
<td>(Deprecated) Style overrides. Width is used in place of dynamically calculated width.</td>
294300
</tr>
295301
</tbody>
296302
</table>

packages/react-spectrum-charts/src/stories/components/Annotation/Annotation.story.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,18 @@ FixedWidthBar.args = {
8181
// color: 'operatingSystem',
8282
};
8383

84-
export { HorizontalBarChart, VerticalBarChart, FixedWidthBar };
84+
const ReducedPadding = BarAnnotationStory.bind({});
85+
ReducedPadding.args = {
86+
textKey: 'percentLabel',
87+
padding: 2,
88+
barOrientation: 'horizontal',
89+
};
90+
91+
const NoPadding = BarAnnotationStory.bind({});
92+
NoPadding.args = {
93+
textKey: 'percentLabel',
94+
padding: 0,
95+
barOrientation: 'horizontal',
96+
};
97+
98+
export { HorizontalBarChart, VerticalBarChart, FixedWidthBar, ReducedPadding, NoPadding };

packages/vega-spec-builder/src/bar/barAnnotationUtils.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* OF ANY KIND, either express or implied. See the License for the specific language
1010
* governing permissions and limitations under the License.
1111
*/
12-
import { FILTERED_TABLE } from '@spectrum-charts/constants';
12+
import { ANNOTATION_PADDING, FILTERED_TABLE } from '@spectrum-charts/constants';
1313

1414
import {
1515
getAnnotationMarks,
@@ -41,32 +41,32 @@ describe('getAnnotationMarks()', () => {
4141

4242
describe('getMinBandwidth()', () => {
4343
test('should be 48 if bar is vertical', () => {
44-
expect(getMinBandwidth('vertical')).toEqual(48);
44+
expect(getMinBandwidth('vertical', ANNOTATION_PADDING)).toEqual(48);
4545
});
4646
test('should be fontsize + 2*padding if bar is horizontal', () => {
47-
expect(getMinBandwidth('horizontal')).toEqual(20);
47+
expect(getMinBandwidth('horizontal', ANNOTATION_PADDING)).toEqual(20);
4848
});
4949
});
5050

5151
describe('getAnnotationXEncode()', () => {
5252
test('should use xc and width if width is defined', () => {
53-
const xEncode = getAnnotationXEncode(100);
53+
const xEncode = getAnnotationXEncode(ANNOTATION_PADDING, 100);
5454
expect(xEncode).toHaveProperty('xc');
5555
expect(xEncode).toHaveProperty('width');
5656
});
5757
test('should use x and x2 if width is not defined', () => {
58-
const xEncode = getAnnotationXEncode();
58+
const xEncode = getAnnotationXEncode(ANNOTATION_PADDING);
5959
expect(xEncode).toHaveProperty('x');
6060
expect(xEncode).toHaveProperty('x2');
6161
});
6262
});
6363

6464
describe('getAnnotationWidth()', () => {
6565
test('should return hardcoded signal if width is defined', () => {
66-
expect(getAnnotationWidth('textLabel', { width: 100 })).toEqual({ value: 100 });
66+
expect(getAnnotationWidth('textLabel', ANNOTATION_PADDING, { width: 100 })).toEqual({ value: 100 });
6767
});
6868
test('should return signal that gets the label width if width is not defined', () => {
69-
const width = getAnnotationWidth('textLabel');
69+
const width = getAnnotationWidth('textLabel', ANNOTATION_PADDING);
7070
expect(width).toHaveProperty('signal');
7171

7272
expect((width as { signal: string }).signal).toContain('getLabelWidth(datum.textLabel,');
@@ -75,17 +75,17 @@ describe('getAnnotationWidth()', () => {
7575

7676
describe('getAnnotationPositionOffset()', () => {
7777
test('returns 12.5 for vertical orientation', () => {
78-
expect(getAnnotationPositionOffset(defaultBarOptions, { value: 12345 })).toEqual('12.5');
78+
expect(getAnnotationPositionOffset(defaultBarOptions, { value: 12345 }, ANNOTATION_PADDING)).toEqual('12.5');
7979
});
8080

8181
test('returns provided value / 2 + 2.5 when value is set and orientation is not vertical', () => {
82-
expect(getAnnotationPositionOffset({ ...defaultBarOptions, orientation: 'horizontal' }, { value: 50 })).toEqual(
82+
expect(getAnnotationPositionOffset({ ...defaultBarOptions, orientation: 'horizontal' }, { value: 50 }, ANNOTATION_PADDING)).toEqual(
8383
'27.5'
8484
);
8585
});
8686

8787
test('returns the signal string wrapped with parens when signal is set and orientation is not vertical', () => {
88-
expect(getAnnotationPositionOffset({ ...defaultBarOptions, orientation: 'horizontal' }, { signal: 'foo' })).toEqual(
88+
expect(getAnnotationPositionOffset({ ...defaultBarOptions, orientation: 'horizontal' }, { signal: 'foo' }, ANNOTATION_PADDING)).toEqual(
8989
'((foo) / 2 + 2.5)'
9090
);
9191
});
@@ -95,7 +95,7 @@ describe('getAnnotationMetricAxisPosition()', () => {
9595
const defaultAnnotationWidth = { value: 22 };
9696

9797
test("defaultBarOptions, should return '${value}1' field", () => {
98-
expect(getAnnotationMetricAxisPosition(defaultBarOptions, defaultAnnotationWidth)).toStrictEqual([
98+
expect(getAnnotationMetricAxisPosition(defaultBarOptions, defaultAnnotationWidth, ANNOTATION_PADDING)).toStrictEqual([
9999
{
100100
signal: `max(scale('yLinear', datum.${defaultBarOptions.metric}1), scale('yLinear', 0) + 12.5)`,
101101
test: `datum.${defaultBarOptions.metric}1 < 0`,
@@ -105,7 +105,7 @@ describe('getAnnotationMetricAxisPosition()', () => {
105105
});
106106
test('horizontal orientation, should return with xLinear scale and min/max properties flipped', () => {
107107
expect(
108-
getAnnotationMetricAxisPosition({ ...defaultBarOptions, orientation: 'horizontal' }, defaultAnnotationWidth)
108+
getAnnotationMetricAxisPosition({ ...defaultBarOptions, orientation: 'horizontal' }, defaultAnnotationWidth, ANNOTATION_PADDING)
109109
).toStrictEqual([
110110
{
111111
test: `datum.${defaultBarOptions.metric}1 < 0`,
@@ -115,7 +115,7 @@ describe('getAnnotationMetricAxisPosition()', () => {
115115
]);
116116
});
117117
test("stacked with seconday scale, should return '${value}1' field", () => {
118-
expect(getAnnotationMetricAxisPosition(defaultBarOptionsWithSecondayColor, defaultAnnotationWidth)).toStrictEqual([
118+
expect(getAnnotationMetricAxisPosition(defaultBarOptionsWithSecondayColor, defaultAnnotationWidth, ANNOTATION_PADDING)).toStrictEqual([
119119
{
120120
signal: `max(scale('yLinear', datum.${defaultBarOptions.metric}1), scale('yLinear', 0) + 12.5)`,
121121
test: `datum.${defaultBarOptions.metric}1 < 0`,
@@ -125,7 +125,7 @@ describe('getAnnotationMetricAxisPosition()', () => {
125125
});
126126
test("dodged without secondary scale, should return 'value' field", () => {
127127
expect(
128-
getAnnotationMetricAxisPosition({ ...defaultBarOptions, type: 'dodged' }, defaultAnnotationWidth)
128+
getAnnotationMetricAxisPosition({ ...defaultBarOptions, type: 'dodged' }, defaultAnnotationWidth, ANNOTATION_PADDING)
129129
).toStrictEqual([
130130
{
131131
signal: `max(scale('yLinear', datum.${defaultBarOptions.metric}), scale('yLinear', 0) + 12.5)`,
@@ -135,7 +135,7 @@ describe('getAnnotationMetricAxisPosition()', () => {
135135
]);
136136
});
137137
test("dodged with secondary scale, should return '${value}1' field", () => {
138-
expect(getAnnotationMetricAxisPosition(defaultBarOptionsWithSecondayColor, defaultAnnotationWidth)).toStrictEqual([
138+
expect(getAnnotationMetricAxisPosition(defaultBarOptionsWithSecondayColor, defaultAnnotationWidth, ANNOTATION_PADDING)).toStrictEqual([
139139
{
140140
signal: `max(scale('yLinear', datum.${defaultBarOptions.metric}1), scale('yLinear', 0) + 12.5)`,
141141
test: `datum.${defaultBarOptions.metric}1 < 0`,

packages/vega-spec-builder/src/bar/barAnnotationUtils.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ const getAnnotation = (
5555
* @returns BarAnnotationSpecOptions
5656
*/
5757
const applyAnnotationPropDefaults = (
58-
{ textKey, ...options }: BarAnnotationOptions,
58+
{ textKey, padding, ...options }: BarAnnotationOptions,
5959
barOptions: BarSpecOptions,
6060
dataName: string,
6161
dimensionScaleName: string,
6262
dimensionField: string
6363
): BarAnnotationSpecOptions => ({
6464
barOptions,
6565
textKey: textKey || barOptions.metric,
66+
padding: padding ?? ANNOTATION_PADDING,
6667
dataName,
6768
dimensionScaleName,
6869
dimensionField,
@@ -106,6 +107,7 @@ export const getAnnotationMarks = (
106107
* @returns TextMark
107108
*/
108109
const getAnnotationTextMark = ({
110+
padding,
109111
barOptions,
110112
dataName,
111113
dimensionField,
@@ -114,8 +116,8 @@ const getAnnotationTextMark = ({
114116
style,
115117
}: BarAnnotationSpecOptions): TextMark => {
116118
const { metricAxis, dimensionAxis } = getOrientationProperties(barOptions.orientation);
117-
const annotationWidth = getAnnotationWidth(textKey, style);
118-
const annotationPosition = getAnnotationMetricAxisPosition(barOptions, annotationWidth);
119+
const annotationWidth = getAnnotationWidth(textKey, padding, style);
120+
const annotationPosition = getAnnotationMetricAxisPosition(barOptions, annotationWidth, padding);
119121

120122
return {
121123
name: `${barOptions.name}_annotationText`,
@@ -133,7 +135,7 @@ const getAnnotationTextMark = ({
133135
[metricAxis]: annotationPosition,
134136
text: [
135137
{
136-
test: `bandwidth('${dimensionScaleName}') > ${getMinBandwidth(barOptions.orientation)}`,
138+
test: `bandwidth('${dimensionScaleName}') > ${getMinBandwidth(barOptions.orientation, padding)}`,
137139
field: textKey,
138140
},
139141
],
@@ -154,6 +156,7 @@ const getAnnotationTextMark = ({
154156
const getAnnotationBackgroundMark = ({
155157
barOptions,
156158
dimensionScaleName,
159+
padding,
157160
textKey,
158161
style,
159162
}: BarAnnotationSpecOptions): RectMark => ({
@@ -164,14 +167,15 @@ const getAnnotationBackgroundMark = ({
164167
interactive: false,
165168
encode: {
166169
enter: {
167-
...getAnnotationXEncode(style?.width),
168-
y: { signal: `datum.bounds.y1 - ${ANNOTATION_PADDING}` },
169-
y2: { signal: `datum.bounds.y2 + ${ANNOTATION_PADDING}` },
170+
...getAnnotationXEncode(padding, style?.width),
171+
y: { signal: `datum.bounds.y1 - ${padding}` },
172+
y2: { signal: `datum.bounds.y2 + ${padding}` },
170173
cornerRadius: { value: 4 },
171174
fill: [
172175
{
173176
test: `datum.datum.${textKey} && bandwidth('${dimensionScaleName}') > ${getMinBandwidth(
174-
barOptions.orientation
177+
barOptions.orientation,
178+
padding
175179
)}`,
176180
signal: BACKGROUND_COLOR,
177181
},
@@ -185,32 +189,32 @@ const getAnnotationBackgroundMark = ({
185189
* @param orientation
186190
* @returns number
187191
*/
188-
export const getMinBandwidth = (orientation: Orientation): number =>
189-
orientation === 'vertical' ? 48 : ANNOTATION_FONT_SIZE + 2 * ANNOTATION_PADDING;
192+
export const getMinBandwidth = (orientation: Orientation, padding: number): number =>
193+
orientation === 'vertical' ? 48 : ANNOTATION_FONT_SIZE + 2 * padding;
190194

191195
/**
192196
* Gets the x position encoding for the annotation background
193197
* @param width
194198
* @returns RectEncodeEntry
195199
*/
196-
export const getAnnotationXEncode = (width?: number): RectEncodeEntry => {
200+
export const getAnnotationXEncode = (padding: number, width?: number): RectEncodeEntry => {
197201
if (width) {
198202
return {
199203
xc: { signal: '(datum.bounds.x1 + datum.bounds.x2) / 2' },
200204
width: { value: width },
201205
};
202206
}
203207
return {
204-
x: { signal: `datum.bounds.x1 - ${ANNOTATION_PADDING}` },
205-
x2: { signal: `datum.bounds.x2 + ${ANNOTATION_PADDING}` },
208+
x: { signal: `datum.bounds.x1 - ${padding}` },
209+
x2: { signal: `datum.bounds.x2 + ${padding}` },
206210
};
207211
};
208212

209-
export const getAnnotationWidth = (textKey: string, style?: BarAnnotationStyleOptions): AnnotationWidth => {
213+
export const getAnnotationWidth = (textKey: string, padding: number, style?: BarAnnotationStyleOptions): AnnotationWidth => {
210214
if (style?.width) return { value: style.width };
211215
return {
212216
signal: `getLabelWidth(datum.${textKey}, '${ANNOTATION_FONT_WEIGHT}', ${ANNOTATION_FONT_SIZE}) + ${
213-
2 * ANNOTATION_PADDING
217+
2 * padding
214218
}`,
215219
};
216220
};
@@ -223,12 +227,13 @@ export const getAnnotationWidth = (textKey: string, style?: BarAnnotationStyleOp
223227
*/
224228
export const getAnnotationPositionOffset = (
225229
{ orientation }: BarSpecOptions,
226-
annotationWidth: AnnotationWidth
230+
annotationWidth: AnnotationWidth,
231+
padding: number
227232
): string => {
228233
const pixelGapFromBaseline = 2.5;
229234

230235
if (orientation === 'vertical') {
231-
return `${(2 * ANNOTATION_PADDING + ANNOTATION_FONT_SIZE) / 2 + pixelGapFromBaseline}`;
236+
return `${(2 * padding + ANNOTATION_FONT_SIZE) / 2 + pixelGapFromBaseline}`;
232237
}
233238

234239
if ('value' in annotationWidth) {
@@ -249,12 +254,13 @@ export const getAnnotationPositionOffset = (
249254
*/
250255
export const getAnnotationMetricAxisPosition = (
251256
options: BarSpecOptions,
252-
annotationWidth: AnnotationWidth
257+
annotationWidth: AnnotationWidth,
258+
padding: number
253259
): ProductionRule<NumericValueRef> => {
254260
const { type, metric, orientation } = options;
255261
const field = type === 'stacked' || isDodgedAndStacked(options) ? `${metric}1` : metric;
256262
const { metricScaleKey: scaleKey } = getOrientationProperties(orientation);
257-
const positionOffset = getAnnotationPositionOffset(options, annotationWidth);
263+
const positionOffset = getAnnotationPositionOffset(options, annotationWidth, padding);
258264

259265
if (orientation === 'vertical') {
260266
return [

packages/vega-spec-builder/src/types/marks/supplemental/barAnnotationSpec.types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ export interface BarAnnotationStyleOptions {
1717
}
1818

1919
export interface BarAnnotationOptions {
20+
/** Padding around annotation text. A small amount of padding is always applied based on content */
21+
padding?: number
2022
/** The key in the data that has the text to display */
2123
textKey?: string;
2224
/** @deprecated */
2325
style?: BarAnnotationStyleOptions;
2426
}
2527

26-
type BarAnnotationOptionsWithDefaults = 'textKey';
28+
type BarAnnotationOptionsWithDefaults = 'textKey' | 'padding';
2729

2830
export interface BarAnnotationSpecOptions
2931
extends PartiallyRequired<BarAnnotationOptions, BarAnnotationOptionsWithDefaults> {

0 commit comments

Comments
 (0)