Skip to content

Commit 26b1d65

Browse files
authored
perf(xy): avoid computing bar value display width (#2754)
This commit improves a bit the geometries rendering logic by avoiding some computations. In particular it avoids computing the bar values to display if the option is not enabled.
1 parent 028de2e commit 26b1d65

File tree

4 files changed

+82
-62
lines changed

4 files changed

+82
-62
lines changed

packages/charts/src/chart_types/xy_chart/rendering/bars.ts

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { BarSeriesStyle, DisplayValueStyle } from '../../../utils/themes/th
1919
import { IndexedGeometryMap } from '../utils/indexed_geometry_map';
2020
import type { DataSeries, DataSeriesDatum, XYChartSeriesIdentifier } from '../utils/series';
2121
import { getSeriesIdentifierFromDataSeries } from '../utils/series';
22-
import type { BarStyleAccessor, DisplayValueSpec, StackMode } from '../utils/specs';
22+
import type { BarStyleAccessor, DisplayValueSpec, StackMode, TickFormatter } from '../utils/specs';
2323
import { LabelOverflowConstraint } from '../utils/specs';
2424

2525
const PADDING = 1; // default padding for now
@@ -30,6 +30,11 @@ type BarTuple = {
3030
indexedGeometryMap: IndexedGeometryMap;
3131
};
3232

33+
/** @internal */
34+
type DisplayValueSpecWithValueFormatter = Omit<DisplayValueSpec, 'valueFormatter'> & {
35+
valueFormatter: TickFormatter;
36+
};
37+
3338
/** @internal */
3439
export function renderBars(
3540
measureText: TextMeasure,
@@ -43,17 +48,17 @@ export function renderBars(
4348
color: Color,
4449
isBandedSpec: boolean,
4550
sharedSeriesStyle: BarSeriesStyle,
46-
displayValueSettings?: DisplayValueSpec,
51+
displayValueSettings?: DisplayValueSpecWithValueFormatter,
4752
styleAccessor?: BarStyleAccessor,
4853
stackMode?: StackMode,
4954
): BarTuple {
50-
const { fontSize, fontFamily } = sharedSeriesStyle.displayValue;
5155
const initialBarTuple: BarTuple = { barGeometries: [], indexedGeometryMap: new IndexedGeometryMap() } as BarTuple;
5256
const y1Fn = getY1ScaledValueFn(yScale);
5357
const y0Fn = getY0ScaledValueFn(yScale);
54-
58+
const seriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries);
5559
return dataSeries.data.reduce((barTuple: BarTuple, datum) => {
5660
const xScaled = xScale.scale(datum.x);
61+
5762
if (!xScale.isValueInDomain(datum.x) || Number.isNaN(xScaled)) {
5863
return barTuple; // don't create a bar if not within the xScale domain
5964
}
@@ -77,7 +82,6 @@ export function renderBars(
7782
// the actual height of the bar
7883
const height = yDiff + addedMinBarHeight;
7984

80-
const seriesIdentifier: XYChartSeriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries);
8185
const seriesStyle = getBarStyleOverrides(datum, seriesIdentifier, sharedSeriesStyle, styleAccessor);
8286

8387
const maxPixelWidth = clamp(seriesStyle.rect.widthRatio ?? 1, 0, 1) * xScale.bandwidth;
@@ -87,52 +91,15 @@ export function renderBars(
8791
const x = xScaled + xScale.bandwidth * orderIndex + xScale.bandwidth / 2 - width / 2;
8892

8993
const y1Value = getDatumYValue(datum, false, isBandedSpec, stackMode);
90-
const formattedDisplayValue = displayValueSettings?.valueFormatter?.(y1Value);
91-
92-
// only show displayValue for even bars if showOverlappingValue
93-
const displayValueText =
94-
displayValueSettings?.isAlternatingValueLabel && barGeometries.length % 2 ? undefined : formattedDisplayValue;
95-
96-
const { displayValueWidth, fixedFontScale } = computeBoxWidth(displayValueText ?? '', {
97-
padding: PADDING,
98-
fontSize,
99-
fontFamily,
100-
measureText,
101-
});
102-
103-
const isHorizontalRotation = chartRotation % 180 === 0;
104-
// Pick the right side of the label's box to use as factor reference
105-
const referenceWidth = Math.max(isHorizontalRotation ? displayValueWidth : fixedFontScale, 1);
106-
107-
const textScalingFactor = getFinalFontScalingFactor(
108-
(width * FONT_SIZE_FACTOR) / referenceWidth,
109-
fixedFontScale,
110-
fontSize,
111-
);
112-
const overflowConstraints: Set<LabelOverflowConstraint> = new Set(
113-
displayValueSettings?.overflowConstraints ?? [
114-
LabelOverflowConstraint.ChartEdges,
115-
LabelOverflowConstraint.BarGeometry,
116-
],
117-
);
118-
119-
// Based on rotation scale the width of the text box
120-
const bboxWidthFactor = isHorizontalRotation ? textScalingFactor : 1;
121-
122-
const displayValue: BarGeometry['displayValue'] | undefined =
123-
displayValueText && displayValueSettings?.showValueLabel
124-
? {
125-
fontScale: textScalingFactor,
126-
fontSize: fixedFontScale,
127-
text: displayValueText,
128-
width: bboxWidthFactor * displayValueWidth,
129-
height: textScalingFactor * fixedFontScale,
130-
overflowConstraints,
131-
}
132-
: undefined;
13394

95+
const shouldDisplayValue =
96+
displayValueSettings?.showValueLabel &&
97+
// only show displayValue for even bars if isAlternatingValueLabel
98+
!(displayValueSettings?.isAlternatingValueLabel && barGeometries.length % 2);
13499
const barGeometry: BarGeometry = {
135-
displayValue,
100+
displayValue: shouldDisplayValue
101+
? computeDisplayValue(y1Value, sharedSeriesStyle, measureText, chartRotation, width, displayValueSettings)
102+
: undefined,
136103
x,
137104
y: yScreenSpaceCoord,
138105
transform: { x: 0, y: 0 },
@@ -169,6 +136,57 @@ export function renderBars(
169136
}, initialBarTuple);
170137
}
171138

139+
function computeDisplayValue(
140+
y1Value: number | null,
141+
sharedSeriesStyle: BarSeriesStyle,
142+
measureText: TextMeasure,
143+
chartRotation: number,
144+
width: number,
145+
displayValueSettings: DisplayValueSpecWithValueFormatter,
146+
): BarGeometry['displayValue'] | undefined {
147+
const { fontSize, fontFamily } = sharedSeriesStyle.displayValue;
148+
const displayValueText = displayValueSettings.valueFormatter(y1Value);
149+
150+
if (displayValueText.length === 0) {
151+
return;
152+
}
153+
154+
const { displayValueWidth, fixedFontScale } = computeBoxWidth(displayValueText, {
155+
padding: PADDING,
156+
fontSize,
157+
fontFamily,
158+
measureText,
159+
});
160+
161+
const isHorizontalRotation = chartRotation % 180 === 0;
162+
// Pick the right side of the label's box to use as factor reference
163+
const referenceWidth = Math.max(isHorizontalRotation ? displayValueWidth : fixedFontScale, 1);
164+
165+
const textScalingFactor = getFinalFontScalingFactor(
166+
(width * FONT_SIZE_FACTOR) / referenceWidth,
167+
fixedFontScale,
168+
fontSize,
169+
);
170+
const overflowConstraints: Set<LabelOverflowConstraint> = new Set(
171+
displayValueSettings?.overflowConstraints ?? [
172+
LabelOverflowConstraint.ChartEdges,
173+
LabelOverflowConstraint.BarGeometry,
174+
],
175+
);
176+
177+
// Based on rotation scale the width of the text box
178+
const bboxWidthFactor = isHorizontalRotation ? textScalingFactor : 1;
179+
180+
return {
181+
fontScale: textScalingFactor,
182+
fontSize: fixedFontScale,
183+
text: displayValueText,
184+
width: bboxWidthFactor * displayValueWidth,
185+
height: textScalingFactor * fixedFontScale,
186+
overflowConstraints,
187+
};
188+
}
189+
172190
/**
173191
* Workout the text box size and fixedFontSize based on a collection of options
174192
* @internal
@@ -189,6 +207,10 @@ function computeBoxWidth(
189207
): { fixedFontScale: number; displayValueWidth: number } {
190208
const fixedFontScale = Math.max(typeof fontSize === 'number' ? fontSize : fontSize.min, 1);
191209

210+
if (text.length === 0) {
211+
return { fixedFontScale, displayValueWidth: 0 };
212+
}
213+
192214
const computedDisplayValueWidth = measureText(
193215
text,
194216
{ fontFamily, fontWeight: 'normal', fontStyle: 'normal', fontVariant: 'normal' },

packages/charts/src/chart_types/xy_chart/rendering/points.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,35 +62,34 @@ export function renderPoints(
6262
const needSorting = !markSizeOptions.enabled;
6363

6464
let style = buildPointGeometryStyles(color, pointStyle);
65-
let styleOverrides: RecursivePartial<PointStyle> | undefined = undefined;
65+
const seriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries);
6666
const { pointGeometries, minDistanceBetweenPoints } = dataSeries.data.reduce<{
6767
pointGeometries: SortedArray<PointGeometry>;
6868
minDistanceBetweenPoints: number;
6969
prevX: number | undefined;
7070
}>(
7171
(acc, datum, dataIndex) => {
7272
const { x: xValue, mark } = datum;
73-
const prev = dataSeries.data[dataIndex - 1];
74-
const next = dataSeries.data[dataIndex + 1];
7573
// don't create the point if not within the xScale domain
7674
if (!xScale.isValueInDomain(xValue)) return acc;
77-
7875
// don't create the point if it that point was filled
7976
const x = xScale.scale(xValue);
80-
8177
if (!isFiniteNumber(x)) return acc;
8278

79+
const prev = dataSeries.data[dataIndex - 1];
80+
const next = dataSeries.data[dataIndex + 1];
81+
8382
if (isFiniteNumber(acc.prevX) && !isDatumFilled(datum)) {
8483
acc.minDistanceBetweenPoints = Math.min(acc.minDistanceBetweenPoints, Math.abs(x - acc.prevX));
8584
}
8685
acc.prevX = x;
8786

8887
const yDatumKeyNames: Array<keyof Omit<FilledValues, 'x'>> = isBandedSpec ? ['y0', 'y1'] : ['y1'];
89-
const seriesIdentifier: XYChartSeriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries);
9088

9189
const isPointIsolated = considerIsolatedPoints
9290
? isIsolatedPoint(dataIndex, dataSeries.data.length, datum, isBandedSpec, y1Fn, y0Fn, prev, next)
9391
: false;
92+
let styleOverrides: RecursivePartial<PointStyle> | undefined = undefined;
9493
if (styleAccessor) {
9594
styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, isPointIsolated, styleAccessor);
9695
style = buildPointGeometryStyles(color, pointStyle, styleOverrides);

packages/charts/src/chart_types/xy_chart/utils/specs.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,6 @@ export interface DisplayValueSpec {
386386
isAlternatingValueLabel?: boolean;
387387
/**
388388
* Function for formatting values; will use axis tickFormatter if none specified
389-
* @defaultValue false
390389
*/
391390
valueFormatter?: TickFormatter;
392391
/**

packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ export type TextMeasure = (text: string, font: Omit<Font, 'textColor'>, fontSize
2323

2424
/** @internal */
2525
export function measureText(ctx: CanvasRenderingContext2D): TextMeasure {
26-
return (text, font, fontSize, lineHeight = 1) =>
27-
withContext(ctx, (ctx): Size => {
28-
if (text.length === 0) {
29-
// TODO this is a temporary fix to make the multilayer time axis work
30-
return { width: 0, height: fontSize * lineHeight };
31-
}
26+
return (text, font, fontSize, lineHeight = 1) => {
27+
if (text.length === 0) {
28+
return { width: 0, height: fontSize * lineHeight };
29+
}
30+
return withContext(ctx, (ctx): Size => {
3231
ctx.font = cssFontShorthand(font, fontSize);
3332
const { width } = ctx.measureText(text);
3433
return { width, height: fontSize * lineHeight };
3534
});
35+
};
3636
}

0 commit comments

Comments
 (0)