Skip to content

Commit 4d40936

Browse files
authored
feat(axis): add tickLabelPadding prop (#217)
* add tickLabelPadding prop * add theme tickLabelPadding * suppress canvas padding manipulation * specify tickLabelPadding from axisSpec or axisConfig * isolate spec and theme in tickLabelPadding story * add AxisStyle interface #94
1 parent 1380651 commit 4d40936

File tree

12 files changed

+157
-26
lines changed

12 files changed

+157
-26
lines changed

src/components/react_canvas/axis.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ export class Axis extends React.PureComponent<AxisProps> {
2929
return this.renderAxis();
3030
}
3131
renderTickLabel = (tick: AxisTick, i: number) => {
32-
const { padding, ...labelStyle } = this.props.chartTheme.axes.tickLabelStyle;
32+
/**
33+
* padding is already computed through width
34+
* and bbox_calculator using tickLabelPadding
35+
* set padding to 0 to avoid conflict
36+
*/
37+
const labelStyle = {
38+
...this.props.chartTheme.axes.tickLabelStyle,
39+
padding: 0,
40+
};
41+
3342
const {
3443
axisSpec: { tickSize, tickPadding, position },
3544
axisTicksDimensions,
@@ -48,6 +57,7 @@ export class Axis extends React.PureComponent<AxisProps> {
4857
);
4958

5059
const { maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions;
60+
5161
const centeredRectProps = centerRotationOrigin(axisTicksDimensions, {
5262
x: tickLabelProps.x,
5363
y: tickLabelProps.y,

src/lib/axes/axis_utils.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { XDomain } from '../series/domains/x_domain';
22
import { YDomain } from '../series/domains/y_domain';
3-
import { AxisSpec, DomainRange, Position } from '../series/specs';
3+
import { AxisSpec, DomainRange, Position, AxisStyle } from '../series/specs';
44
import { LIGHT_THEME } from '../themes/light_theme';
55
import { AxisId, getAxisId, getGroupId, GroupId } from '../utils/ids';
66
import { ScaleType } from '../utils/scales/scales';
@@ -28,6 +28,7 @@ import {
2828
isVertical,
2929
isYDomain,
3030
mergeDomainsByGroupId,
31+
getAxisTickLabelPadding,
3132
} from './axis_utils';
3233
import { CanvasTextBBoxCalculator } from './canvas_text_bbox_calculator';
3334
import { SvgTextBBoxCalculator } from './svg_text_bbox_calculator';
@@ -458,7 +459,7 @@ describe('Axis computational utils', () => {
458459
});
459460
test('should get max bbox dimensions for a tick in comparison to previous values', () => {
460461
const bboxCalculator = new CanvasTextBBoxCalculator();
461-
const reducer = getMaxBboxDimensions(bboxCalculator, 16, 'Arial', 0);
462+
const reducer = getMaxBboxDimensions(bboxCalculator, 16, 'Arial', 0, 1);
462463

463464
const accWithGreaterValues = {
464465
maxLabelBboxWidth: 100,
@@ -1272,4 +1273,24 @@ describe('Axis computational utils', () => {
12721273
expect(isBounded(lowerBounded)).toBe(true);
12731274
expect(isBounded(upperBounded)).toBe(true);
12741275
});
1276+
test('should not allow negative padding', () => {
1277+
const negativePadding = -2;
1278+
// value canvas_text_bbox_calculator changes negative values is 1
1279+
const positivePadding = 1;
1280+
1281+
const bboxCalculator = new CanvasTextBBoxCalculator();
1282+
const negativeReducer = getMaxBboxDimensions(bboxCalculator, 16, 'Arial', 0, negativePadding);
1283+
const positiveReducer = getMaxBboxDimensions(bboxCalculator, 16, 'Arial', 0, positivePadding);
1284+
1285+
expect(JSON.stringify(negativeReducer)).toEqual(JSON.stringify(positiveReducer));
1286+
});
1287+
test('should expect axisSpec.style.tickLabelPadding if specified', () => {
1288+
const axisSpecStyle: AxisStyle = {
1289+
tickLabelPadding: 2,
1290+
};
1291+
1292+
const axisConfigTickLabelPadding = 1;
1293+
1294+
expect(getAxisTickLabelPadding(axisConfigTickLabelPadding, axisSpecStyle)).toEqual(2);
1295+
});
12751296
});

src/lib/axes/axis_utils.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Rotation,
1111
TickFormatter,
1212
UpperBoundedDomain,
13+
AxisStyle,
1314
} from '../series/specs';
1415
import { AxisConfig, Theme } from '../themes/theme';
1516
import { Dimensions, Margins } from '../utils/dimensions';
@@ -44,7 +45,7 @@ export interface TickLabelProps {
4445
/**
4546
* Compute the ticks values and identify max width and height of the labels
4647
* so we can compute the max space occupied by the axis component.
47-
* @param axisSpec tbe spec of the axis
48+
* @param axisSpec the spec of the axis
4849
* @param xDomain the x domain associated
4950
* @param yDomain the y domain array
5051
* @param totalBarsInCluster the total number of grouped series
@@ -69,11 +70,15 @@ export function computeAxisTicksDimensions(
6970
if (!scale) {
7071
throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`);
7172
}
73+
74+
const tickLabelPadding = getAxisTickLabelPadding(axisConfig.tickLabelStyle.padding, axisSpec.style);
75+
7276
const dimensions = computeTickDimensions(
7377
scale,
7478
axisSpec.tickFormat,
7579
bboxCalculator,
7680
axisConfig,
81+
tickLabelPadding,
7782
axisSpec.tickLabelRotation,
7883
);
7984

@@ -82,6 +87,13 @@ export function computeAxisTicksDimensions(
8287
};
8388
}
8489

90+
export function getAxisTickLabelPadding(axisConfigTickLabelPadding: number, axisSpecStyle?: AxisStyle): number {
91+
if (axisSpecStyle && axisSpecStyle.tickLabelPadding !== undefined) {
92+
return axisSpecStyle.tickLabelPadding;
93+
}
94+
return axisConfigTickLabelPadding;
95+
}
96+
8597
export function isYDomain(position: Position, chartRotation: Rotation): boolean {
8698
const isStraightRotation = chartRotation === 0 || chartRotation === 180;
8799
if (isVertical(position)) {
@@ -133,6 +145,7 @@ export const getMaxBboxDimensions = (
133145
fontSize: number,
134146
fontFamily: string,
135147
tickLabelRotation: number,
148+
tickLabelPadding: number,
136149
) => (
137150
acc: { [key: string]: number },
138151
tickLabel: string,
@@ -142,7 +155,7 @@ export const getMaxBboxDimensions = (
142155
maxLabelTextWidth: number;
143156
maxLabelTextHeight: number;
144157
} => {
145-
const bbox = bboxCalculator.compute(tickLabel, fontSize, fontFamily).getOrElse({
158+
const bbox = bboxCalculator.compute(tickLabel, tickLabelPadding, fontSize, fontFamily).getOrElse({
146159
width: 0,
147160
height: 0,
148161
});
@@ -158,7 +171,6 @@ export const getMaxBboxDimensions = (
158171
const prevHeight = acc.maxLabelBboxHeight;
159172
const prevLabelWidth = acc.maxLabelTextWidth;
160173
const prevLabelHeight = acc.maxLabelTextHeight;
161-
162174
return {
163175
maxLabelBboxWidth: prevWidth > width ? prevWidth : width,
164176
maxLabelBboxHeight: prevHeight > height ? prevHeight : height,
@@ -172,6 +184,7 @@ function computeTickDimensions(
172184
tickFormat: TickFormatter,
173185
bboxCalculator: BBoxCalculator,
174186
axisConfig: AxisConfig,
187+
tickLabelPadding: number,
175188
tickLabelRotation: number = 0,
176189
) {
177190
const tickValues = scale.ticks();
@@ -182,7 +195,7 @@ function computeTickDimensions(
182195
} = axisConfig;
183196

184197
const { maxLabelBboxWidth, maxLabelBboxHeight, maxLabelTextWidth, maxLabelTextHeight } = tickLabels.reduce(
185-
getMaxBboxDimensions(bboxCalculator, fontSize, fontFamily, tickLabelRotation),
198+
getMaxBboxDimensions(bboxCalculator, fontSize, fontFamily, tickLabelRotation, tickLabelPadding),
186199
{ maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 },
187200
);
188201

src/lib/axes/bbox_calculator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export interface BBox {
66
}
77

88
export interface BBoxCalculator {
9-
compute(text: string, fontSize?: number, fontFamily?: string): Option<BBox>;
9+
compute(text: string, padding: number, fontSize?: number, fontFamily?: string): Option<BBox>;
1010
destroy(): void;
1111
}

src/lib/axes/canvas_text_bbox_calculator.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import { CanvasTextBBoxCalculator } from './canvas_text_bbox_calculator';
44
describe('CanvasTextBBoxCalculator', () => {
55
test('can create a canvas for computing text measurement values', () => {
66
const canvasBboxCalculator = new CanvasTextBBoxCalculator();
7-
const bbox = canvasBboxCalculator.compute('foo').getOrElse({
7+
const bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
88
width: 0,
99
height: 0,
1010
});
1111
expect(Math.abs(bbox.width - 23.2)).toBeLessThanOrEqual(2);
1212
expect(bbox.height).toBe(16);
1313

1414
canvasBboxCalculator.context = null;
15-
expect(canvasBboxCalculator.compute('foo')).toBe(none);
15+
expect(canvasBboxCalculator.compute('foo', 0)).toBe(none);
1616
});
1717
test('can compute near the same width for the same text independently of the scale factor', () => {
1818
let canvasBboxCalculator = new CanvasTextBBoxCalculator(undefined, 5);
1919

20-
let bbox = canvasBboxCalculator.compute('foo').getOrElse({
20+
let bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
2121
width: 0,
2222
height: 0,
2323
});
@@ -26,7 +26,7 @@ describe('CanvasTextBBoxCalculator', () => {
2626

2727
canvasBboxCalculator = new CanvasTextBBoxCalculator(undefined, 10);
2828

29-
bbox = canvasBboxCalculator.compute('foo').getOrElse({
29+
bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
3030
width: 0,
3131
height: 0,
3232
});
@@ -35,7 +35,7 @@ describe('CanvasTextBBoxCalculator', () => {
3535

3636
canvasBboxCalculator = new CanvasTextBBoxCalculator(undefined, 50);
3737

38-
bbox = canvasBboxCalculator.compute('foo').getOrElse({
38+
bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
3939
width: 0,
4040
height: 0,
4141
});
@@ -44,7 +44,7 @@ describe('CanvasTextBBoxCalculator', () => {
4444

4545
canvasBboxCalculator = new CanvasTextBBoxCalculator(undefined, 100);
4646

47-
bbox = canvasBboxCalculator.compute('foo').getOrElse({
47+
bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
4848
width: 0,
4949
height: 0,
5050
});
@@ -53,7 +53,7 @@ describe('CanvasTextBBoxCalculator', () => {
5353

5454
canvasBboxCalculator = new CanvasTextBBoxCalculator(undefined, 1000);
5555

56-
bbox = canvasBboxCalculator.compute('foo').getOrElse({
56+
bbox = canvasBboxCalculator.compute('foo', 0).getOrElse({
5757
width: 0,
5858
height: 0,
5959
});

src/lib/axes/canvas_text_bbox_calculator.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ export class CanvasTextBBoxCalculator implements BBoxCalculator {
1717
this.attachedRoot.appendChild(this.offscreenCanvas);
1818
this.scaledFontSize = scaledFontSize;
1919
}
20-
compute(text: string, fontSize = 16, fontFamily = 'Arial', padding: number = 1): Option<BBox> {
20+
compute(text: string, padding: number, fontSize = 16, fontFamily = 'Arial'): Option<BBox> {
2121
if (!this.context) {
2222
return none;
2323
}
2424

25+
// Padding should be at least one to avoid browser measureText inconsistencies
26+
if (padding < 1) {
27+
padding = 1;
28+
}
29+
2530
// We scale the text up to get a more accurate computation of the width of the text
2631
// because `measureText` can vary a lot between browsers.
2732
const scalingFactor = this.scaledFontSize / fontSize;

src/lib/series/rendering.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ export function renderBars(
207207
const barGeometries: BarGeometry[] = [];
208208

209209
const bboxCalculator = new CanvasTextBBoxCalculator();
210+
// default padding to 1 for now
211+
const padding = 1;
210212
const fontSize = seriesStyle && seriesStyle.displayValue ? seriesStyle.displayValue.fontSize : undefined;
211213
const fontFamily = seriesStyle && seriesStyle.displayValue ? seriesStyle.displayValue.fontFamily : undefined;
212214

@@ -253,10 +255,12 @@ export function renderBars(
253255
: undefined
254256
: formattedDisplayValue;
255257

256-
const computedDisplayValueWidth = bboxCalculator.compute(displayValueText || '', fontSize, fontFamily).getOrElse({
257-
width: 0,
258-
height: 0,
259-
}).width;
258+
const computedDisplayValueWidth = bboxCalculator
259+
.compute(displayValueText || '', padding, fontSize, fontFamily)
260+
.getOrElse({
261+
width: 0,
262+
height: 0,
263+
}).width;
260264
const displayValueWidth =
261265
displayValueSettings && displayValueSettings.isValueContainedInElement ? width : computedDisplayValueWidth;
262266

src/lib/series/specs.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,17 @@ export interface AxisSpec {
218218
title?: string;
219219
/** If specified, it constrains the domain for these values */
220220
domain?: DomainRange;
221+
/** Object to hold custom styling */
222+
style?: AxisStyle;
221223
}
222224

223225
export type TickFormatter = (value: any) => string;
224226

227+
export interface AxisStyle {
228+
/** Specifies the amount of padding on the tick label bounding box */
229+
tickLabelPadding?: number;
230+
}
231+
225232
/**
226233
* The position of the axis relative to the chart.
227234
* A left or right positioned axis is a vertical axis.

src/lib/themes/dark_theme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const DARK_THEME: Theme = {
9292
fontFamily: 'sans-serif',
9393
fontStyle: 'normal',
9494
fill: 'white',
95-
padding: 0,
95+
padding: 1,
9696
},
9797
tickLineStyle: {
9898
stroke: 'white',

src/lib/themes/light_theme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const LIGHT_THEME: Theme = {
9292
fontFamily: 'sans-serif',
9393
fontStyle: 'normal',
9494
fill: 'gray',
95-
padding: 0,
95+
padding: 1,
9696
},
9797
tickLineStyle: {
9898
stroke: 'gray',

0 commit comments

Comments
 (0)