diff --git a/api/charts.api.md b/api/charts.api.md index 28256bb17f..067efa733c 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -593,9 +593,13 @@ export interface DisplayValueSpec { } // @public (undocumented) -export type DisplayValueStyle = Omit & { +export type DisplayValueStyle = Omit & { offsetX: number; offsetY: number; + fontSize: number | { + min: number; + max: number; + }; fill: Color | { color: Color; borderColor?: Color; diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-value-label-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-value-label-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..f66046ba96 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-value-label-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-valuelabel-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-valuelabel-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..f66046ba96 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-simple-valuelabel-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index e1527877b6..b1e4f162de 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index 627189a0f6..41b2288284 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index 3d8859cddb..74d009fb4a 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-0-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index 35ff068da4..0d2e20d6dd 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-bottom-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index 3ff694a3f9..298fb1b631 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-middle-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png index 2efc6b58c8..2b3c076056 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-value-labels-positioning-rotation-180-vertical-alignment-top-horizontal-alignment-left-place-the-value-labels-on-the-correct-area-1-snap.png differ diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts index e18415d434..126381b8b8 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts @@ -37,6 +37,7 @@ export function renderText( }, degree: number = 0, translation?: Partial, + scale: number = 1, ) { if (text === undefined || text === null) { return; @@ -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); }); }); } @@ -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; } diff --git a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts index a85d1b9c8c..561eed7d15 100644 --- a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -47,14 +47,14 @@ const CHART_DIRECTION: Record = { /** @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, @@ -109,6 +109,8 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP shadowSize, }, -chartRotation, + undefined, + fontScale, ); } } diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 0b0a3eb4be..805e89ffc4 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -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'; @@ -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 @@ -307,6 +368,7 @@ export function renderBars( styleAccessor?: BarStyleAccessor, minBarHeight?: number, stackMode?: StackMode, + chartRotation?: number, ): { barGeometries: BarGeometry[]; indexedGeometryMap: IndexedGeometryMap; @@ -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, } diff --git a/src/chart_types/xy_chart/state/utils/utils.ts b/src/chart_types/xy_chart/state/utils/utils.ts index 40537fb4da..89613577e6 100644 --- a/src/chart_types/xy_chart/state/utils/utils.ts +++ b/src/chart_types/xy_chart/state/utils/utils.ts @@ -358,6 +358,7 @@ export function computeSeriesGeometries( axesSpecs, chartTheme, enableHistogramMode, + chartRotation, stackMode, ); orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + 1 : orderIndex; @@ -398,6 +399,7 @@ export function computeSeriesGeometries( axesSpecs, chartTheme, enableHistogramMode, + chartRotation, ); orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + counts[SeriesTypes.Bar] : orderIndex; @@ -499,6 +501,7 @@ function renderGeometries( axesSpecs: AxisSpec[], chartTheme: Theme, enableHistogramMode: boolean, + chartRotation: number, stackMode?: StackMode, ): { points: PointGeometry[]; @@ -563,6 +566,7 @@ function renderGeometries( spec.styleAccessor, spec.minBarHeight, stackMode, + chartRotation, ); indexedGeometryMap.merge(renderedBars.indexedGeometryMap); bars.push(...renderedBars.barGeometries); diff --git a/src/mocks/geometries.ts b/src/mocks/geometries.ts index 27393e87a4..4474e120a4 100644 --- a/src/mocks/geometries.ts +++ b/src/mocks/geometries.ts @@ -70,6 +70,7 @@ export class MockBarGeometry { height: 0, color, displayValue: { + fontSize: 10, text: '', width: 0, height: 0, diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 5e6ad42c3e..ed9ed1b5a0 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -73,6 +73,8 @@ export interface BarGeometry { height: number; color: Color; displayValue?: { + fontScale?: number; + fontSize: number; text: any; width: number; height: number; diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 594d1f939f..4571b166df 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -290,9 +290,15 @@ export interface Theme { export type PartialTheme = RecursivePartial; /** @public */ -export type DisplayValueStyle = Omit & { +export type DisplayValueStyle = Omit & { offsetX: number; offsetY: number; + fontSize: + | number + | { + min: number; + max: number; + }; fill: | Color | { color: Color; borderColor?: Color; borderWidth?: number } diff --git a/stories/bar/51_label_value_advanced.tsx b/stories/bar/51_label_value_advanced.tsx index 6b617bf6c9..ffe1001366 100644 --- a/stories/bar/51_label_value_advanced.tsx +++ b/stories/bar/51_label_value_advanced.tsx @@ -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,