Skip to content

Commit

Permalink
feat(bar_chart): scaled font size for value labels (#789)
Browse files Browse the repository at this point in the history
Bar charts value labels are automatically scaled up to best fit the bar area and improve readability. The auto scaling can be limited to upper and lower bounds if required.
The scaling takes into account chart rotation and box hiding computation has been reworked to adapt to the new mechanism.

fix: #788

BREAKING CHANGE: The `DisplayValueStyle` `fontSize` property can now express an upper and lower bound as size, used for the automatic scaling.
  • Loading branch information
Wylie Conlon authored Oct 14, 2020
1 parent 9b29392 commit 3bdd1ee
Show file tree
Hide file tree
Showing 17 changed files with 125 additions and 20 deletions.
6 changes: 5 additions & 1 deletion api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,13 @@ export interface DisplayValueSpec {
}

// @public (undocumented)
export type DisplayValueStyle = Omit<TextStyle, 'fill'> & {
export type DisplayValueStyle = Omit<TextStyle, 'fill' | 'fontSize'> & {
offsetX: number;
offsetY: number;
fontSize: number | {
min: number;
max: number;
};
fill: Color | {
color: Color;
borderColor?: Color;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 8 additions & 5 deletions src/chart_types/xy_chart/renderer/canvas/primitives/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function renderText(
},
degree: number = 0,
translation?: Partial<Point>,
scale: number = 1,
) {
if (text === undefined || text === null) {
return;
Expand All @@ -51,15 +52,17 @@ export function renderText(
if (translation?.x || translation?.y) {
ctx.translate(translation?.x ?? 0, translation?.y ?? 0);
}
ctx.translate(origin.x, origin.y);
ctx.scale(scale, scale);
if (font.shadow) {
ctx.lineJoin = 'round';
const prevLineWidth = ctx.lineWidth;
ctx.lineWidth = font.shadowSize || 1.5;
ctx.strokeStyle = font.shadow;
ctx.strokeText(text, origin.x, origin.y);
ctx.strokeText(text, 0, 0);
ctx.lineWidth = prevLineWidth;
}
ctx.fillText(text, origin.x, origin.y);
ctx.fillText(text, 0, 0);
});
});
}
Expand Down Expand Up @@ -95,14 +98,14 @@ export function wrapLines(
const shouldWrap = true;
const textArr: string[] = [];
const textMeasureProcessor = measureText(ctx);
const getTextWidth = (text: string) => {
const getTextWidth = (textString: string) => {
const measuredText = textMeasureProcessor(fontSize, [
{
text,
text: textString,
...font,
},
]);
const measure = measuredText[0];
const [measure] = measuredText;
if (measure) {
return measure.width;
}
Expand Down
6 changes: 4 additions & 2 deletions src/chart_types/xy_chart/renderer/canvas/values/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ const CHART_DIRECTION: Record<string, Rotation> = {
/** @internal */
export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesProps) {
const { bars, debug, chartRotation, chartDimensions, theme } = props;
const { fontFamily, fontStyle, fill, fontSize, alignment } = theme.barSeriesStyle.displayValue;
const { fontFamily, fontStyle, fill, alignment } = theme.barSeriesStyle.displayValue;
const barsLength = bars.length;
for (let i = 0; i < barsLength; i++) {
const { displayValue } = bars[i];
if (!displayValue) {
continue;
}
const { text } = displayValue;
const { text, fontSize, fontScale } = displayValue;
let textLines = {
lines: [text],
width: displayValue.width,
Expand Down Expand Up @@ -109,6 +109,8 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP
shadowSize,
},
-chartRotation,
undefined,
fontScale,
);
}
}
Expand Down
97 changes: 87 additions & 10 deletions src/chart_types/xy_chart/rendering/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
GeometryStateStyle,
LineStyle,
BubbleSeriesStyle,
DisplayValueStyle,
} from '../../../utils/themes/theme';
import { IndexedGeometryMap, GeometryType } from '../utils/indexed_geometry_map';
import { DataSeriesDatum, DataSeries, XYChartSeriesIdentifier } from '../utils/series';
Expand All @@ -56,6 +57,66 @@ export interface MarkSizeOptions {
enabled: boolean;
ratio?: number;
}
/**
* Returns a safe scaling factor for label text for fixed or range size inputs
* @internal
*/
function getFinalFontScalingFactor(
scale: number,
fixedFontSize: number,
limits: DisplayValueStyle['fontSize'],
): number {
if (typeof limits === 'number') {
// it's a fixed size, so it's always ok
return 1;
}
const finalFontSize = scale * fixedFontSize;
if (finalFontSize > limits.max) {
return limits.max / fixedFontSize;
}
if (finalFontSize < limits.min) {
// it's technically 1, but keep it generic in case the fixedFontSize changes
return limits.min / fixedFontSize;
}
return scale;
}

/**
* Workout the text box size and fixedFontSize based on a collection of options
* @internal
*/
function computeBoxWidth(
text: string,
{
padding,
fontSize,
fontFamily,
bboxCalculator,
width,
}: {
padding: number;
fontSize: number | { min: number; max: number };
fontFamily: string;
bboxCalculator: CanvasTextBBoxCalculator;
width: number;
},
displayValueSettings: DisplayValueSpec | undefined,
): { fixedFontScale: number; displayValueWidth: number } {
const fixedFontScale = Math.max(typeof fontSize === 'number' ? fontSize : fontSize.min, 1);

const computedDisplayValueWidth = bboxCalculator.compute(text || '', padding, fixedFontScale, fontFamily).width;
if (typeof fontSize !== 'number') {
return {
fixedFontScale,
displayValueWidth: computedDisplayValueWidth,
};
}
return {
fixedFontScale,
displayValueWidth:
displayValueSettings && displayValueSettings.isValueContainedInElement ? width : computedDisplayValueWidth,
};
}

/**
* Returns value of `y1` or `filled.y1` or null
Expand Down Expand Up @@ -307,6 +368,7 @@ export function renderBars(
styleAccessor?: BarStyleAccessor,
minBarHeight?: number,
stackMode?: StackMode,
chartRotation?: number,
): {
barGeometries: BarGeometry[];
indexedGeometryMap: IndexedGeometryMap;
Expand Down Expand Up @@ -385,25 +447,40 @@ export function renderBars(

// only show displayValue for even bars if showOverlappingValue
const displayValueText =
displayValueSettings && displayValueSettings.isAlternatingValueLabel
? barGeometries.length % 2 === 0
? formattedDisplayValue
: undefined
displayValueSettings && displayValueSettings.isAlternatingValueLabel && barGeometries.length % 2
? undefined
: formattedDisplayValue;

const computedDisplayValueWidth = bboxCalculator.compute(displayValueText || '', padding, fontSize, fontFamily)
.width;
const displayValueWidth =
displayValueSettings && displayValueSettings.isValueContainedInElement ? width : computedDisplayValueWidth;
const { displayValueWidth, fixedFontScale } = computeBoxWidth(
displayValueText || '',
{ padding, fontSize, fontFamily, bboxCalculator, width },
displayValueSettings,
);

const isHorizontalRotation = chartRotation == null || [0, 180].includes(chartRotation);
// Take 70% of space for the label text
const fontSizeFactor = 0.7;
// Pick the right side of the label's box to use as factor reference
const referenceWidth = Math.max(isHorizontalRotation ? displayValueWidth : fixedFontScale, 1);

const textScalingFactor = getFinalFontScalingFactor(
(width * fontSizeFactor) / referenceWidth,
fixedFontScale,
fontSize,
);

const hideClippedValue = displayValueSettings ? displayValueSettings.hideClippedValue : undefined;
// Based on rotation scale the width of the text box
const bboxWidthFactor = isHorizontalRotation ? textScalingFactor : 1;

const displayValue =
displayValueSettings && displayValueSettings.showValueLabel
? {
fontScale: textScalingFactor,
fontSize: fixedFontScale,
text: displayValueText,
width: displayValueWidth,
height: fontSize,
width: bboxWidthFactor * displayValueWidth,
height: textScalingFactor * fixedFontScale,
hideClippedValue,
isValueContainedInElement: displayValueSettings.isValueContainedInElement,
}
Expand Down
4 changes: 4 additions & 0 deletions src/chart_types/xy_chart/state/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ export function computeSeriesGeometries(
axesSpecs,
chartTheme,
enableHistogramMode,
chartRotation,
stackMode,
);
orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + 1 : orderIndex;
Expand Down Expand Up @@ -398,6 +399,7 @@ export function computeSeriesGeometries(
axesSpecs,
chartTheme,
enableHistogramMode,
chartRotation,
);
orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + counts[SeriesTypes.Bar] : orderIndex;

Expand Down Expand Up @@ -499,6 +501,7 @@ function renderGeometries(
axesSpecs: AxisSpec[],
chartTheme: Theme,
enableHistogramMode: boolean,
chartRotation: number,
stackMode?: StackMode,
): {
points: PointGeometry[];
Expand Down Expand Up @@ -563,6 +566,7 @@ function renderGeometries(
spec.styleAccessor,
spec.minBarHeight,
stackMode,
chartRotation,
);
indexedGeometryMap.merge(renderedBars.indexedGeometryMap);
bars.push(...renderedBars.barGeometries);
Expand Down
1 change: 1 addition & 0 deletions src/mocks/geometries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class MockBarGeometry {
height: 0,
color,
displayValue: {
fontSize: 10,
text: '',
width: 0,
height: 0,
Expand Down
2 changes: 2 additions & 0 deletions src/utils/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export interface BarGeometry {
height: number;
color: Color;
displayValue?: {
fontScale?: number;
fontSize: number;
text: any;
width: number;
height: number;
Expand Down
8 changes: 7 additions & 1 deletion src/utils/themes/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,15 @@ export interface Theme {
export type PartialTheme = RecursivePartial<Theme>;

/** @public */
export type DisplayValueStyle = Omit<TextStyle, 'fill'> & {
export type DisplayValueStyle = Omit<TextStyle, 'fill' | 'fontSize'> & {
offsetX: number;
offsetY: number;
fontSize:
| number
| {
min: number;
max: number;
};
fill:
| Color
| { color: Color; borderColor?: Color; borderWidth?: number }
Expand Down
8 changes: 7 additions & 1 deletion stories/bar/51_label_value_advanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ export const Example = () => {
const borderColor = color('value border color', 'rgba(0,0,0,1)');
const borderSize = number('value border width', 1.5);

const fixedFontSize = number('Fixed font size', 10);
const useFixedFontSize = boolean('Use fixed font size', false);

const maxFontSize = number('Max font size', 25);
const minFontSize = number('Min font size', 10);

const theme = {
barSeriesStyle: {
displayValue: {
fontSize: number('value font size', 10),
fontSize: useFixedFontSize ? fixedFontSize : { max: maxFontSize, min: minFontSize },
fontFamily: "'Open Sans', Helvetica, Arial, sans-serif",
fontStyle: 'normal',
padding: 0,
Expand Down

0 comments on commit 3bdd1ee

Please sign in to comment.