From e796ee7df9ac578c316eeea75d0ad9ec16d9bd2a Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Wed, 1 Apr 2026 03:23:42 +0200 Subject: [PATCH 1/9] Add 'Elastic UI Numeric' font assets and helpers. --- .../ElasticUINumeric-Variable.woff2 | Bin 0 -> 7912 bytes public/fonts/elastic_ui_numeric/LICENSE.txt | 104 ++++++++++++++++++ .../utils/elastic_ui_numeric_font.scss | 8 ++ .../stories/utils/elastic_ui_numeric_font.ts | 50 +++++++++ 4 files changed, 162 insertions(+) create mode 100644 public/fonts/elastic_ui_numeric/ElasticUINumeric-Variable.woff2 create mode 100644 public/fonts/elastic_ui_numeric/LICENSE.txt create mode 100644 storybook/stories/utils/elastic_ui_numeric_font.scss create mode 100644 storybook/stories/utils/elastic_ui_numeric_font.ts diff --git a/public/fonts/elastic_ui_numeric/ElasticUINumeric-Variable.woff2 b/public/fonts/elastic_ui_numeric/ElasticUINumeric-Variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..feb9713c743555a7d08ec7b34780576238cf6981 GIT binary patch literal 7912 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 S-TbT(_; + +function isRecord(value: unknown): value is MutableRecord { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +export function prependNumericFontFamily(fontFamily: string) { + return fontFamily.includes(ELASTIC_UI_NUMERIC_FONT_FAMILY) + ? fontFamily + : `${ELASTIC_UI_NUMERIC_FONT_FAMILY}, ${fontFamily}`; +} + +export function withOptionalNumericFontFamily(fontFamily: string, enabled: boolean) { + return enabled ? prependNumericFontFamily(fontFamily) : fontFamily; +} + +export function applyNumericFontFamily(value: unknown): void { + if (Array.isArray(value)) { + value.forEach(applyNumericFontFamily); + return; + } + + if (!isRecord(value)) return; + + for (const [key, entry] of Object.entries(value)) { + if (key === 'fontFamily' && typeof entry === 'string') { + value[key] = prependNumericFontFamily(entry); + continue; + } + + applyNumericFontFamily(entry); + } +} + +export function applyOptionalNumericFontFamily(value: unknown, enabled: boolean): void { + if (!enabled) return; + applyNumericFontFamily(value); +} From c2c5a6ee905deb04c285893474e0df544d66877a Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Wed, 1 Apr 2026 03:29:02 +0200 Subject: [PATCH 2/9] Add font measurement story. --- .../test_cases/34_font_measurement.story.tsx | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 storybook/stories/test_cases/34_font_measurement.story.tsx diff --git a/storybook/stories/test_cases/34_font_measurement.story.tsx b/storybook/stories/test_cases/34_font_measurement.story.tsx new file mode 100644 index 00000000000..b64d5abe545 --- /dev/null +++ b/storybook/stories/test_cases/34_font_measurement.story.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean, number, select } from '@storybook/addon-knobs'; +import React from 'react'; + +import type { Datum, PartialTheme } from '@elastic/charts'; +import { + Axis, + BarSeries, + Chart, + Heatmap, + LabelOverflowConstraint, + Metric, + Partition, + PartitionLayout, + Position, + ScaleType, + Settings, + defaultPartitionValueFormatter, +} from '@elastic/charts'; + +import type { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { applyOptionalNumericFontFamily, withOptionalNumericFontFamily } from '../utils/elastic_ui_numeric_font'; + +const metricData = [ + [ + { + color: '#3c3c3c', + title: 'Revenue 2025', + subtitle: 'Total Annual Revenue', + value: 5678901.23, + valueFormatter: (v: number) => `$${v.toFixed(2)}`, + }, + ], +]; + +const barData = [ + { x: '2020', y: 1234567, g: 'Product Alpha - $1,234,567' }, + { x: '2021', y: 2345678, g: 'Product Alpha - $1,234,567' }, + { x: '2022', y: 3456789, g: 'Product Alpha - $1,234,567' }, + { x: '2023', y: 4567890, g: 'Product Alpha - $1,234,567' }, + { x: '2024', y: 5678901, g: 'Product Alpha - $1,234,567' }, + { x: '2020', y: 987654, g: 'Product Beta - $987,654' }, + { x: '2021', y: 1876543, g: 'Product Beta - $987,654' }, + { x: '2022', y: 2765432, g: 'Product Beta - $987,654' }, + { x: '2023', y: 3654321, g: 'Product Beta - $987,654' }, + { x: '2024', y: 4543210, g: 'Product Beta - $987,654' }, + { x: '2020', y: 567890, g: 'Product Gamma - $567,890' }, + { x: '2021', y: 1456789, g: 'Product Gamma - $567,890' }, + { x: '2022', y: 2345678, g: 'Product Gamma - $567,890' }, + { x: '2023', y: 3234567, g: 'Product Gamma - $567,890' }, + { x: '2024', y: 4123456, g: 'Product Gamma - $567,890' }, +]; + +const treemapData = [ + { region: 'North America', product: 'Electronics', revenue: 4500000 }, + { region: 'North America', product: 'Clothing', revenue: 2300000 }, + { region: 'Europe', product: 'Electronics', revenue: 3800000 }, + { region: 'Europe', product: 'Clothing', revenue: 1900000 }, + { region: 'Europe', product: 'Food', revenue: 2700000 }, + { region: 'Asia Pacific', product: 'Electronics', revenue: 5200000 }, + { region: 'Asia Pacific', product: 'Clothing', revenue: 3100000 }, + { region: 'Asia Pacific', product: 'Food', revenue: 1600000 }, + { region: 'Latin America', product: 'Electronics', revenue: 1800000 }, + { region: 'Latin America', product: 'Clothing', revenue: 900000 }, +]; + +const regionColors: Record = { + 'North America': '#3B528B', + Europe: '#24868E', + 'Asia Pacific': '#35B779', + 'Latin America': '#AADC32', +}; + +const heatmapData = (() => { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const categories = ['Product A', 'Product B', 'Product C', 'Product D', 'Product E']; + return categories.flatMap((category, categoryIndex) => + months.map((month, monthIndex) => ({ + x: month, + y: category, + value: 12000 + categoryIndex * 9000 + monthIndex * 3700 + ((categoryIndex + monthIndex) % 3) * 850, + })), + ); +})(); + +export const Example: ChartsStory = (_, { description }) => { + const baseTheme = useBaseTheme(); + const fontFamily = select( + 'Font: family', + { Inter: 'Inter', Arial: 'Arial', 'Times New Roman': 'Times New Roman', Courier: 'Courier' }, + 'Inter', + ); + const fontSize = number('Font: size (px)', 14, { range: true, min: 8, max: 48, step: 1 }); + const useElasticUINumericFont = boolean('Font: use "Elastic UI Numeric"', true); + const letterSpacing = number('Typography: letter-spacing (px)', 0, { range: true, min: -2, max: 5, step: 0.5 }); + const fontKerning = select('Typography: font-kerning', { auto: 'auto', normal: 'normal', none: 'none' }, 'auto'); + const previewFontFamily = withOptionalNumericFontFamily(fontFamily, useElasticUINumericFont); + + const containerStyle: React.CSSProperties = { + ...(letterSpacing !== 0 ? { letterSpacing: `${letterSpacing}px` } : {}), + ...(fontKerning !== 'auto' ? { fontKerning } : {}), + }; + + const theme: PartialTheme = { + metric: { + fontFamily, + }, + legend: { + fontFamily, + }, + barSeriesStyle: { + displayValue: { + fontSize: fontSize + 2, + fontFamily, + }, + }, + axes: { + tickLabel: { + fontSize, + fontFamily, + }, + }, + partition: { + fillLabel: { + fontFamily, + valueFont: { + fontFamily, + }, + }, + }, + heatmap: { + xAxisLabel: { + fontFamily, + }, + yAxisLabel: { + fontFamily, + width: 'auto', + padding: { left: 8, right: 8 }, + }, + cell: { + maxWidth: 'fill', + label: { + visible: true, + minFontSize: 8, + maxFontSize: 14, + useGlobalMinFontSize: true, + fontFamily, + }, + border: { stroke: 'white', strokeWidth: 1 }, + }, + }, + }; + applyOptionalNumericFontFamily(theme, useElasticUINumericFont); + + const activeTypography = [ + ...(useElasticUINumericFont ? ['Elastic UI Numeric font'] : []), + ...(letterSpacing !== 0 ? [`letter-spacing: ${letterSpacing}px`] : []), + ...(fontKerning !== 'auto' ? [`font-kerning: ${fontKerning}`] : []), + ]; + + const resizableChart = (height: number): React.CSSProperties => ({ + width: 600, + height, + resize: 'both', + overflow: 'hidden', + border: '1px solid #d3dae6', + borderRadius: '4px', + }); + + const sectionTitle: React.CSSProperties = { + margin: 0, + fontSize: '13px', + fontWeight: 600, + }; + + return ( +
+ {activeTypography.length > 0 && ( +
+ Active typography: {activeTypography.join(' | ')} +
+ 0123456789 - $1,234,567.89 - 100.00% +
+
+ )} + +

{description}

+ +
+

Metric

+
+ + + + +
+
+ +
+

Bar chart with legend and display values

+
+ + + + `$${(d / 1_000_000).toFixed(1)}M`} + /> + `$${(d / 1_000_000).toFixed(2)}M`, + overflowConstraints: [LabelOverflowConstraint.ChartEdges, LabelOverflowConstraint.BarGeometry], + }} + xScaleType={ScaleType.Ordinal} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={['y']} + splitSeriesAccessors={['g']} + stackAccessors={['x']} + data={barData} + /> + +
+
+ +
+

Treemap

+
+ + + d.revenue as number} + valueFormatter={(d: number) => `$${defaultPartitionValueFormatter(Math.round(d / 1000))}\u00A0K`} + layers={[ + { + groupByRollup: (d: Datum) => d.region, + nodeLabel: (d: Datum) => String(d), + fillLabel: { + valueFormatter: (d: number) => `$${defaultPartitionValueFormatter(Math.round(d / 1000))}\u00A0K`, + }, + shape: { fillColor: (key: string) => regionColors[key] ?? '#888' }, + }, + { + groupByRollup: (d: Datum) => d.product, + nodeLabel: (d: Datum) => String(d), + fillLabel: { + valueFormatter: (d: number) => `$${defaultPartitionValueFormatter(Math.round(d / 1000))}\u00A0K`, + }, + shape: { + fillColor: (key: string, _shapeDepth: number, _node: unknown, tree) => { + const parent = tree.length > 1 ? tree[tree.length - 2] : undefined; + const parentKey = Array.isArray(parent) ? String(parent[0]) : ''; + return regionColors[parentKey] ?? '#aaa'; + }, + }, + }, + ]} + /> + +
+
+ +
+

Heatmap

+
+ + + d.toLocaleString()} + xSortPredicate="dataIndex" + /> + +
+
+
+ ); +}; + +Example.parameters = { + resize: false, +}; From 649894574960188aa009fa9ef0741c717827ba39 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Wed, 1 Apr 2026 13:47:56 +0200 Subject: [PATCH 3/9] Add support for a custom font family for bullet charts. --- packages/charts/api/charts.api.md | 2 + .../bullet_graph/renderer/canvas/bullet.ts | 21 ++++--- .../renderer/canvas/sub_types/angular.ts | 7 ++- .../renderer/canvas/sub_types/horizontal.ts | 7 ++- .../renderer/canvas/sub_types/vertical.ts | 5 +- .../bullet_graph/selectors/get_layout.ts | 47 +++++++------- .../src/chart_types/bullet_graph/theme.ts | 61 +++++++++++-------- .../src/utils/themes/amsterdam_dark_theme.ts | 1 + .../src/utils/themes/amsterdam_light_theme.ts | 1 + 9 files changed, 87 insertions(+), 65 deletions(-) diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 7b878f5e81e..0966ff87e3b 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -567,6 +567,8 @@ export interface BulletStyle { // (undocumented) fallbackBandColor: Color; // (undocumented) + fontFamily: string; + // (undocumented) minHeight: Pixels; // (undocumented) nonFiniteText: string; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet.ts index 3b9852853bc..6dfdef72b3d 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet.ts +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet.ts @@ -22,14 +22,14 @@ import type { BulletStyle } from '../../theme'; import { FONT_PADDING, HEADER_PADDING, - SUBTITLE_FONT, + getSubtitleFont, + getTargetFont, + getTitleFont, + getValueFont, SUBTITLE_FONT_SIZE, - TARGET_FONT, TARGET_FONT_SIZE, - TITLE_FONT, TITLE_FONT_SIZE, TITLE_LINE_SPACING, - VALUE_FONT, VALUE_FONT_SIZE, getMaxTargetValueAssent, getTextAscentHeight, @@ -51,6 +51,11 @@ export function renderBullet( ) { const { debug, style, dimensions, activeValues, spec, backgroundColor } = props; withContext(ctx, (ctx) => { + const titleFont = getTitleFont(style.fontFamily); + const subtitleFont = getSubtitleFont(style.fontFamily); + const valueFont = getValueFont(style.fontFamily); + const targetFont = getTargetFont(style.fontFamily); + ctx.scale(dpr, dpr); clearCanvas(ctx, backgroundColor); @@ -112,7 +117,7 @@ export function renderBullet( // Title ctx.fillStyle = props.style.textColor; ctx.textAlign = 'start'; - ctx.font = cssFontShorthand(TITLE_FONT, TITLE_FONT_SIZE); + ctx.font = cssFontShorthand(titleFont, TITLE_FONT_SIZE); const titleYBaseline = commonYBaseline - @@ -129,12 +134,12 @@ export function renderBullet( // Subtitle if (bulletGraph.subtitle) { - ctx.font = cssFontShorthand(SUBTITLE_FONT, SUBTITLE_FONT_SIZE); + ctx.font = cssFontShorthand(subtitleFont, SUBTITLE_FONT_SIZE); ctx.fillText(bulletGraph.subtitle, 0, commonYBaseline); } // Value - ctx.font = cssFontShorthand(VALUE_FONT, VALUE_FONT_SIZE); + ctx.font = cssFontShorthand(valueFont, VALUE_FONT_SIZE); if (!multiline) ctx.textAlign = 'end'; { const y = commonYBaseline + (multiline ? MAX_TARGET_VALUE_ASCENT + FONT_PADDING : 0); @@ -145,7 +150,7 @@ export function renderBullet( // Target if (bulletGraph.target) { - ctx.font = cssFontShorthand(TARGET_FONT, TARGET_FONT_SIZE); + ctx.font = cssFontShorthand(targetFont, TARGET_FONT_SIZE); if (!multiline) ctx.textAlign = 'end'; const x = multiline ? bulletGraph.valueWidth : bulletGraph.header.width; const y = commonYBaseline + (multiline ? MAX_TARGET_VALUE_ASCENT + FONT_PADDING : 0); diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts index a39c41fc0c3..1bb282b439c 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts @@ -18,7 +18,7 @@ import type { BulletPanelDimensions } from '../../../selectors/get_panel_dimensi import type { BulletSpec } from '../../../spec'; import { BulletSubtype } from '../../../spec'; import type { BulletStyle } from '../../../theme'; -import { GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { GRAPH_PADDING, TICK_FONT_SIZE, getTickFont } from '../../../theme'; import { getAngledChartSizing } from '../../../utils/angular'; import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH } from '../constants'; @@ -32,6 +32,7 @@ export function angularBullet( debug: boolean, activeValue?: ActiveValue | null, ) { + const tickFont = getTickFont(style.fontFamily); const { datum, graphArea, scale, ticks, colorBands } = dimensions; const { radius } = getAngledChartSizing(graphArea.size, spec.subtype); const [startAngle, endAngle] = scale.range() as [number, number]; @@ -119,14 +120,14 @@ export function angularBullet( const measure = measureText(ctx); // Assumes mostly homogenous formatting const maxTickWidth = formatterColorTicks.reduce((acc, t) => { - const { width } = measure(t.formattedValue, TICK_FONT, TICK_FONT_SIZE); + const { width } = measure(t.formattedValue, tickFont, TICK_FONT_SIZE); return Math.max(acc, width); }, 0); // Tick labels ctx.fillStyle = style.textColor; ctx.textBaseline = 'middle'; - ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + ctx.font = cssFontShorthand(tickFont, TICK_FONT_SIZE); formatterColorTicks .filter((tick) => tick.value >= min && tick.value <= max) .forEach((tick) => { diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts index a56e267ee2d..a9cc88f45c4 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts @@ -14,7 +14,7 @@ import type { ContinuousDomain, GenericDomain } from '../../../../../utils/domai import type { ActiveValue } from '../../../selectors/get_active_values'; import type { BulletPanelDimensions } from '../../../selectors/get_panel_dimensions'; import type { BulletStyle } from '../../../theme'; -import { GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { GRAPH_PADDING, TICK_FONT_SIZE, getTickFont } from '../../../theme'; import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH, TICK_LABEL_PADDING } from '../constants'; /** @internal */ @@ -25,6 +25,7 @@ export function horizontalBullet( backgroundColor: Color, activeValue?: ActiveValue | null, ) { + const tickFont = getTickFont(style.fontFamily); ctx.translate(GRAPH_PADDING.left, 0); const { datum, colorBands, ticks, scale } = dimensions; @@ -89,14 +90,14 @@ export function horizontalBullet( // Tick labels ctx.fillStyle = style.textColor; ctx.textBaseline = 'top'; - ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + ctx.font = cssFontShorthand(tickFont, TICK_FONT_SIZE); ticks .filter((tick) => tick >= min && tick <= max) .forEach((tick, i) => { const labelText = datum.tickFormatter(tick); if (i === ticks.length - 1) { const availableWidth = Math.abs((start > end ? min : max) - (ticks.at(i) ?? NaN)); - const { width: labelWidth } = measureText(ctx)(labelText, TICK_FONT, TICK_FONT_SIZE); + const { width: labelWidth } = measureText(ctx)(labelText, tickFont, TICK_FONT_SIZE); ctx.textAlign = labelWidth >= Math.abs(scale(availableWidth) - scale(0)) ? 'end' : 'start'; } else { ctx.textAlign = 'start'; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts index 061a651d921..6247bdcee5b 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts @@ -13,7 +13,7 @@ import type { ContinuousDomain, GenericDomain } from '../../../../../utils/domai import type { ActiveValue } from '../../../selectors/get_active_values'; import type { BulletPanelDimensions } from '../../../selectors/get_panel_dimensions'; import type { BulletStyle } from '../../../theme'; -import { GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { GRAPH_PADDING, TICK_FONT_SIZE, getTickFont } from '../../../theme'; import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH, TICK_LABEL_PADDING } from '../constants'; /** @internal */ @@ -24,6 +24,7 @@ export function verticalBullet( backgroundColor: Color, activeValue?: ActiveValue | null, ) { + const tickFont = getTickFont(style.fontFamily); ctx.translate(0, GRAPH_PADDING.top); const { datum, graphArea, scale, colorBands, ticks } = dimensions; @@ -99,7 +100,7 @@ export function verticalBullet( // Tick labels ctx.textBaseline = 'top'; ctx.fillStyle = style.textColor; - ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + ctx.font = cssFontShorthand(tickFont, TICK_FONT_SIZE); ticks .filter((tick) => tick >= min && tick <= max) .forEach((tick, i) => { diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts index dfa359c18ff..f8e5517c773 100644 --- a/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts @@ -9,6 +9,7 @@ import { getBulletSpec } from './get_bullet_spec'; import { getChartSize } from './get_chart_size'; import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getChartThemeSelector } from '../../../state/selectors/get_chart_theme'; import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_spec'; import { withTextMeasure } from '../../../utils/bbox/canvas_text_bbox_calculator'; import type { Size } from '../../../utils/dimensions'; @@ -18,14 +19,14 @@ import type { BulletDatum } from '../spec'; import { FONT_PADDING, HEADER_PADDING, - SUBTITLE_FONT, + getSubtitleFont, + getTargetFont, + getTitleFont, + getValueFont, SUBTITLE_FONT_SIZE, - TARGET_FONT, TARGET_FONT_SIZE, - TITLE_FONT, TITLE_FONT_SIZE, TITLE_LINE_SPACING, - VALUE_FONT, VALUE_FONT_SIZE, getMaxTargetValueAssent, getTextAscentHeight, @@ -83,8 +84,8 @@ const minChartWidths: Record = { /** @internal */ export const getLayout = createCustomCachedSelector( - [getBulletSpec, getChartSize, getSettingsSpecSelector], - (spec, chartSize, { locale }): BulletLayout => { + [getBulletSpec, getChartSize, getSettingsSpecSelector, getChartThemeSelector], + (spec, chartSize, { locale }, { bulletGraph }): BulletLayout => { const { data } = spec; const rows = data.length; const columns = data.reduce((acc, row) => { @@ -97,6 +98,11 @@ export const getLayout = createCustomCachedSelector( height: panel.height - HEADER_PADDING.top - HEADER_PADDING.bottom, }; + const titleFont = getTitleFont(bulletGraph.fontFamily); + const subtitleFont = getSubtitleFont(bulletGraph.fontFamily); + const valueFont = getValueFont(bulletGraph.fontFamily); + const targetFont = getTargetFont(bulletGraph.fontFamily); + return withTextMeasure((textMeasurer) => { // collect header elements title, subtitles and values const header = data.map((row) => @@ -111,10 +117,10 @@ export const getLayout = createCustomCachedSelector( datum: cell, }; const widths = { - title: textMeasurer(content.title.trim(), TITLE_FONT, TITLE_FONT_SIZE).width, - subtitle: content.subtitle ? textMeasurer(content.subtitle, TITLE_FONT, TITLE_FONT_SIZE).width : 0, - value: textMeasurer(content.value, VALUE_FONT, VALUE_FONT_SIZE).width, - target: textMeasurer(content.target, TARGET_FONT, TARGET_FONT_SIZE).width, + title: textMeasurer(content.title.trim(), titleFont, TITLE_FONT_SIZE).width, + subtitle: content.subtitle ? textMeasurer(content.subtitle, subtitleFont, SUBTITLE_FONT_SIZE).width : 0, + value: textMeasurer(content.value, valueFont, VALUE_FONT_SIZE).width, + target: textMeasurer(content.target, targetFont, TARGET_FONT_SIZE).width, }; return { content, widths }; }), @@ -135,7 +141,7 @@ export const getLayout = createCustomCachedSelector( const titleTruncated = wrapText( cell.content.title, - TITLE_FONT, + titleFont, TITLE_FONT_SIZE, availableWidth, 2, @@ -143,15 +149,8 @@ export const getLayout = createCustomCachedSelector( locale, ).meta.truncated; const subtitleTruncated = cell.content.subtitle - ? wrapText( - cell.content.subtitle, - SUBTITLE_FONT, - SUBTITLE_FONT_SIZE, - availableWidth, - 1, - textMeasurer, - locale, - ).meta.truncated + ? wrapText(cell.content.subtitle, subtitleFont, SUBTITLE_FONT_SIZE, availableWidth, 1, textMeasurer, locale) + .meta.truncated : false; return titleTruncated || subtitleTruncated; @@ -172,7 +171,7 @@ export const getLayout = createCustomCachedSelector( // wrap only title if necessary title: wrapText( cell.content.title, - TITLE_FONT, + titleFont, TITLE_FONT_SIZE, headerSize.width, 2, @@ -182,7 +181,7 @@ export const getLayout = createCustomCachedSelector( subtitle: cell.content.subtitle ? wrapText( cell.content.subtitle, - SUBTITLE_FONT, + subtitleFont, SUBTITLE_FONT_SIZE, headerSize.width, 1, @@ -203,11 +202,11 @@ export const getLayout = createCustomCachedSelector( return { panel, header: headerSize, - title: wrapText(cell.content.title, TITLE_FONT, TITLE_FONT_SIZE, availableWidth, 2, textMeasurer, locale), + title: wrapText(cell.content.title, titleFont, TITLE_FONT_SIZE, availableWidth, 2, textMeasurer, locale), subtitle: cell.content.subtitle ? wrapText( cell.content.subtitle, - SUBTITLE_FONT, + subtitleFont, SUBTITLE_FONT_SIZE, availableWidth, 1, diff --git a/packages/charts/src/chart_types/bullet_graph/theme.ts b/packages/charts/src/chart_types/bullet_graph/theme.ts index 2c8c0d12053..75ef9cf39a6 100644 --- a/packages/charts/src/chart_types/bullet_graph/theme.ts +++ b/packages/charts/src/chart_types/bullet_graph/theme.ts @@ -23,6 +23,7 @@ import { /** @public */ export interface BulletStyle { + fontFamily: string; textColor: Color; border: Color; barBackground: Color; @@ -38,6 +39,7 @@ export interface BulletStyle { /** @internal */ export const LIGHT_THEME_BULLET_STYLE: BulletStyle = { + fontFamily: DEFAULT_FONT_FAMILY, textColor: LIGHT_TEXT_COLORS.textParagraph, border: LIGHT_BORDER_COLORS.borderBaseSubdued, barBackground: LIGHT_TEXT_COLORS.textParagraph, @@ -50,6 +52,7 @@ export const LIGHT_THEME_BULLET_STYLE: BulletStyle = { /** @internal */ export const DARK_THEME_BULLET_STYLE: BulletStyle = { + fontFamily: DEFAULT_FONT_FAMILY, textColor: DARK_TEXT_COLORS.textParagraph, border: DARK_BORDER_COLORS.borderBaseSubdued, barBackground: DARK_TEXT_COLORS.textParagraph, @@ -60,14 +63,40 @@ export const DARK_THEME_BULLET_STYLE: BulletStyle = { fallbackBandColor: DARK_BACKGROUND_COLORS.backgroundBaseDisabled, }; +function getFont(fontFamily: string, fontWeight: Font['fontWeight']): Font { + return { + fontStyle: 'normal', + fontFamily, + fontVariant: 'normal', + fontWeight, + textColor: 'black', + }; +} + /** @internal */ -export const TITLE_FONT: Font = { - fontStyle: 'normal', - fontFamily: DEFAULT_FONT_FAMILY, - fontVariant: 'normal', - fontWeight: 'bold', - textColor: 'black', -}; +export function getTitleFont(fontFamily: string): Font { + return getFont(fontFamily, 'bold'); +} + +/** @internal */ +export function getSubtitleFont(fontFamily: string): Font { + return getFont(fontFamily, 'normal'); +} + +/** @internal */ +export function getValueFont(fontFamily: string): Font { + return getFont(fontFamily, 'bold'); +} + +/** @internal */ +export function getTargetFont(fontFamily: string): Font { + return getFont(fontFamily, 'normal'); +} + +/** @internal */ +export function getTickFont(fontFamily: string): Font { + return getFont(fontFamily, 'normal'); +} /** * Approximate height of font ascent from the baseline @@ -87,30 +116,17 @@ export const TITLE_FONT_ASCENT = TITLE_FONT_SIZE * TEXT_ASCENT_RATIO; /** @internal */ export const TITLE_LINE_SPACING = 4; -/** @internal */ -export const SUBTITLE_FONT: Font = { - ...TITLE_FONT, - fontWeight: 'normal', -}; /** @internal */ export const SUBTITLE_FONT_SIZE = 14; /** @internal */ export const SUBTITLE_FONT_ASCENT = SUBTITLE_FONT_SIZE * TEXT_ASCENT_RATIO; -/** @internal */ -export const VALUE_FONT: Font = { - ...TITLE_FONT, -}; /** @internal */ export const VALUE_FONT_SIZE = 22; const VALUE_FONT_ASCENT = VALUE_FONT_SIZE * TEXT_ASCENT_RATIO; -/** @internal */ -export const TARGET_FONT: Font = { - ...SUBTITLE_FONT, -}; /** @internal */ export const TARGET_FONT_SIZE = 16; @@ -120,11 +136,6 @@ const TARGET_FONT_ASCENT = TARGET_FONT_SIZE * TEXT_ASCENT_RATIO; export const getMaxTargetValueAssent = (target?: string) => !target ? VALUE_FONT_ASCENT : Math.max(VALUE_FONT_ASCENT, TARGET_FONT_ASCENT); -/** @internal */ -export const TICK_FONT: Font = { - ...TITLE_FONT, - fontWeight: 'normal', -}; /** @internal */ export const TICK_FONT_SIZE = 10; diff --git a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts index c7e7d9edd91..dc526570223 100644 --- a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts +++ b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts @@ -440,6 +440,7 @@ export const AMSTERDAM_DARK_THEME: Theme = { titleWeight: 500, }, bulletGraph: { + fontFamily: DEFAULT_FONT_FAMILY, textColor: '#E0E5EE', border: '#343741', barBackground: '#FFF', diff --git a/packages/charts/src/utils/themes/amsterdam_light_theme.ts b/packages/charts/src/utils/themes/amsterdam_light_theme.ts index e48b6e4620c..95228309b97 100644 --- a/packages/charts/src/utils/themes/amsterdam_light_theme.ts +++ b/packages/charts/src/utils/themes/amsterdam_light_theme.ts @@ -440,6 +440,7 @@ export const AMSTERDAM_LIGHT_THEME: Theme = { titleWeight: 500, }, bulletGraph: { + fontFamily: DEFAULT_FONT_FAMILY, textColor: '#343741', border: '#EDF0F5', barBackground: '#343741', From bf8637d941e1e1f469b37f61403954de6a5bec94 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Wed, 1 Apr 2026 14:36:21 +0200 Subject: [PATCH 4/9] Add 'Elastic UI Numeric' font to bullet graph story. --- storybook/stories/bullet_graph/1_single.story.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/storybook/stories/bullet_graph/1_single.story.tsx b/storybook/stories/bullet_graph/1_single.story.tsx index fdf97422d65..75c532d97c2 100644 --- a/storybook/stories/bullet_graph/1_single.story.tsx +++ b/storybook/stories/bullet_graph/1_single.story.tsx @@ -15,10 +15,12 @@ import { Chart, Bullet, BulletSubtype, Settings } from '@elastic/charts'; import type { ChartsStory } from '../../types'; import { useBaseTheme } from '../../use_base_theme'; import { getDebugStateLogger } from '../utils/debug_state_logger'; +import { withOptionalNumericFontFamily } from '../utils/elastic_ui_numeric_font'; import { customKnobs } from '../utils/knobs'; import { getKnobFromEnum } from '../utils/knobs/utils'; export const Example: ChartsStory = (_, { title, description }) => { + const baseTheme = useBaseTheme(); const debug = boolean('debug', false); const debugState = boolean('Enable debug state', false); const bulletTitle = text('title', 'Error rate', 'General'); @@ -30,6 +32,8 @@ export const Example: ChartsStory = (_, { title, description }) => { const format = text('format (numeraljs)', '0.[0]', 'General'); const formatter = (d: number) => numeral(d).format(format); const subtype = getKnobFromEnum('subtype', BulletSubtype, BulletSubtype.horizontal, { group: 'General' }); + const useElasticUINumericFont = boolean('use "Elastic UI Numeric" font', true, 'General'); + const numericFontFamily = withOptionalNumericFontFamily(baseTheme.bulletGraph.fontFamily, useElasticUINumericFont); const niceDomain = boolean('niceDomain', false, 'Ticks'); const tickStrategy = customKnobs.multiSelect( @@ -57,7 +61,12 @@ export const Example: ChartsStory = (_, { title, description }) => { debug={debug} onRenderChange={getDebugStateLogger(debugState)} debugState={debugState} - baseTheme={useBaseTheme()} + baseTheme={baseTheme} + theme={{ + bulletGraph: { + fontFamily: numericFontFamily, + }, + }} /> Date: Wed, 1 Apr 2026 15:09:34 +0200 Subject: [PATCH 5/9] Font measurement story test. --- e2e/tests/test_cases_stories.test.ts | 15 +++++++++ .../test_cases/34_font_measurement.story.tsx | 32 ++----------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/e2e/tests/test_cases_stories.test.ts b/e2e/tests/test_cases_stories.test.ts index bea7e42df68..d8badab309f 100644 --- a/e2e/tests/test_cases_stories.test.ts +++ b/e2e/tests/test_cases_stories.test.ts @@ -179,4 +179,19 @@ test.describe('Test cases stories', () => { ); }); }); + + test('should render font measurement test story', async ({ page }) => { + await common.expectElementAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/test-cases--font-measurement-test', + '#story-root', + { + waitSelector: common.chartWaitSelector, + action: async () => { + await page.waitForFunction(() => { + return document.querySelectorAll('.echChartStatus[data-ech-render-complete="true"]').length >= 4; + }); + }, + }, + ); + }); }); diff --git a/storybook/stories/test_cases/34_font_measurement.story.tsx b/storybook/stories/test_cases/34_font_measurement.story.tsx index b64d5abe545..c364f45a828 100644 --- a/storybook/stories/test_cases/34_font_measurement.story.tsx +++ b/storybook/stories/test_cases/34_font_measurement.story.tsx @@ -80,7 +80,7 @@ const regionColors: Record = { }; const heatmapData = (() => { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug']; const categories = ['Product A', 'Product B', 'Product C', 'Product D', 'Product E']; return categories.flatMap((category, categoryIndex) => months.map((month, monthIndex) => ({ @@ -105,17 +105,12 @@ export const Example: ChartsStory = (_, { description }) => { const previewFontFamily = withOptionalNumericFontFamily(fontFamily, useElasticUINumericFont); const containerStyle: React.CSSProperties = { + fontFamily: previewFontFamily, ...(letterSpacing !== 0 ? { letterSpacing: `${letterSpacing}px` } : {}), ...(fontKerning !== 'auto' ? { fontKerning } : {}), }; const theme: PartialTheme = { - metric: { - fontFamily, - }, - legend: { - fontFamily, - }, barSeriesStyle: { displayValue: { fontSize: fontSize + 2, @@ -160,12 +155,6 @@ export const Example: ChartsStory = (_, { description }) => { }; applyOptionalNumericFontFamily(theme, useElasticUINumericFont); - const activeTypography = [ - ...(useElasticUINumericFont ? ['Elastic UI Numeric font'] : []), - ...(letterSpacing !== 0 ? [`letter-spacing: ${letterSpacing}px`] : []), - ...(fontKerning !== 'auto' ? [`font-kerning: ${fontKerning}`] : []), - ]; - const resizableChart = (height: number): React.CSSProperties => ({ width: 600, height, @@ -183,23 +172,6 @@ export const Example: ChartsStory = (_, { description }) => { return (
- {activeTypography.length > 0 && ( -
- Active typography: {activeTypography.join(' | ')} -
- 0123456789 - $1,234,567.89 - 100.00% -
-
- )} -

{description}

From 448328e7a4e9d68a8e8e6c08f90dfddf06dcddee Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Thu, 2 Apr 2026 01:08:24 +0200 Subject: [PATCH 6/9] Export font measurement story. --- storybook/stories/test_cases/test_cases.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/storybook/stories/test_cases/test_cases.stories.tsx b/storybook/stories/test_cases/test_cases.stories.tsx index 2da5430f2a5..014d570f690 100644 --- a/storybook/stories/test_cases/test_cases.stories.tsx +++ b/storybook/stories/test_cases/test_cases.stories.tsx @@ -28,3 +28,4 @@ export { Example as pointStyleOverrides } from './13_point_style_overrides.story export { Example as errorBoundary } from './14_error_boundary.story'; export { Example as linearNicing } from './15_linear_nicing.story'; export { Example as lensStressTest } from './33_lens_stress.story'; +export { Example as fontMeasurementTest } from './34_font_measurement.story'; From 2ff918421d8b0927dc10cc98b66fc9df875c65bf Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Thu, 2 Apr 2026 01:17:44 +0200 Subject: [PATCH 7/9] Add 'use "Elastic UI Numeric" font' knob to angular bullet graph story. --- storybook/stories/bullet_graph/2_angular.story.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/storybook/stories/bullet_graph/2_angular.story.tsx b/storybook/stories/bullet_graph/2_angular.story.tsx index 1bf68be2d8e..dbf6774c89b 100644 --- a/storybook/stories/bullet_graph/2_angular.story.tsx +++ b/storybook/stories/bullet_graph/2_angular.story.tsx @@ -14,9 +14,11 @@ import { Chart, Bullet, BulletSubtype, Settings } from '@elastic/charts'; import type { ChartsStory } from '../../types'; import { useBaseTheme } from '../../use_base_theme'; +import { withOptionalNumericFontFamily } from '../utils/elastic_ui_numeric_font'; import { getKnobFromEnum } from '../utils/knobs/utils'; export const Example: ChartsStory = (_, { title, description }) => { + const baseTheme = useBaseTheme(); const debug = boolean('debug', false); const bulletTitle = text('title', 'A Nice Title'); const subtitle = text('subtitle', 'Subtitle'); @@ -31,12 +33,15 @@ export const Example: ChartsStory = (_, { title, description }) => { }); const format = text('format', '0'); const formatter = (d: number) => numeral(d).format(format); + const useElasticUINumericFont = boolean('use "Elastic UI Numeric" font', true); + const numericFontFamily = withOptionalNumericFontFamily(baseTheme.bulletGraph.fontFamily, useElasticUINumericFont); return ( Date: Thu, 2 Apr 2026 01:18:22 +0200 Subject: [PATCH 8/9] Add `font-display: block;` for "Elastic UI Numeric" font' so that stories always get the font. --- storybook/stories/utils/elastic_ui_numeric_font.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storybook/stories/utils/elastic_ui_numeric_font.scss b/storybook/stories/utils/elastic_ui_numeric_font.scss index 1d9a6adbed7..46ad1a78434 100644 --- a/storybook/stories/utils/elastic_ui_numeric_font.scss +++ b/storybook/stories/utils/elastic_ui_numeric_font.scss @@ -2,7 +2,7 @@ font-family: 'Elastic UI Numeric'; font-style: normal; font-weight: 100 900; - font-display: swap; + font-display: block; src: url('../../../public/fonts/elastic_ui_numeric/ElasticUINumeric-Variable.woff2') format('woff2'); unicode-range: U+20, U+24-25, U+28-29, U+2B-2F, U+30-3A, U+A0, U+202F, U+2212; } From f9688badbfd844d158aca809df3a82d5397fd27c Mon Sep 17 00:00:00 2001 From: "elastic-datavis[bot]" <98618603+elastic-datavis[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:10:21 +0000 Subject: [PATCH 9/9] test(vrt): update screenshots [skip ci] --- .../font-measurement-test-chrome-linux.png | Bin 0 -> 10862 bytes ...font-measurement-test-story-chrome-linux.png | Bin 0 -> 30327 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/font-measurement-test-chrome-linux.png create mode 100644 e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/should-render-font-measurement-test-story-chrome-linux.png diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/font-measurement-test-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/font-measurement-test-chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..23b80af2b2692997218a389299c7ede069719b41 GIT binary patch literal 10862 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

R>t`9XtoL0c{L4EM`Z6MxKS4q!3#NSaw1+;_a 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 O0C{PZFF;At(EkOzHV_~H literal 0 HcmV?d00001 diff --git a/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/should-render-font-measurement-test-story-chrome-linux.png b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/should-render-font-measurement-test-story-chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a7828d99c7ea5b701b41532694e5f2dc32131d64 GIT binary patch literal 30327 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