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 00000000000..23b80af2b26 Binary files /dev/null and b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/font-measurement-test-chrome-linux.png differ 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 00000000000..a7828d99c7e Binary files /dev/null and b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/should-render-font-measurement-test-story-chrome-linux.png differ 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/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', 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 00000000000..feb9713c743 Binary files /dev/null and b/public/fonts/elastic_ui_numeric/ElasticUINumeric-Variable.woff2 differ diff --git a/public/fonts/elastic_ui_numeric/LICENSE.txt b/public/fonts/elastic_ui_numeric/LICENSE.txt new file mode 100644 index 00000000000..ed03c797927 --- /dev/null +++ b/public/fonts/elastic_ui_numeric/LICENSE.txt @@ -0,0 +1,104 @@ +Elastic UI Numeric +================== + +This font is a modified version of Inter, created by Elastic for use in Kibana charts. +It contains a subset of numeric glyphs and common punctuation with OpenType features +(tnum, zero, ss01, ss07) baked into the default character mapping. + +Upstream: Inter (https://github.com/rsms/inter) +Copyright: Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) +"Inter" is trademark of Rasmus Andersson. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +This modified font is distributed under the SIL Open Font License, Version 1.1, +the same license as the upstream Inter font. + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. 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, + }, + }} /> { + 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 ( `$${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']; + 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 = { + fontFamily: previewFontFamily, + ...(letterSpacing !== 0 ? { letterSpacing: `${letterSpacing}px` } : {}), + ...(fontKerning !== 'auto' ? { fontKerning } : {}), + }; + + const theme: PartialTheme = { + 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 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 ( +
+

{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, +}; 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'; diff --git a/storybook/stories/utils/elastic_ui_numeric_font.scss b/storybook/stories/utils/elastic_ui_numeric_font.scss new file mode 100644 index 00000000000..46ad1a78434 --- /dev/null +++ b/storybook/stories/utils/elastic_ui_numeric_font.scss @@ -0,0 +1,8 @@ +@font-face { + font-family: 'Elastic UI Numeric'; + font-style: normal; + font-weight: 100 900; + 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; +} diff --git a/storybook/stories/utils/elastic_ui_numeric_font.ts b/storybook/stories/utils/elastic_ui_numeric_font.ts new file mode 100644 index 00000000000..93badcdb0a2 --- /dev/null +++ b/storybook/stories/utils/elastic_ui_numeric_font.ts @@ -0,0 +1,50 @@ +/* + * 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 './elastic_ui_numeric_font.scss'; + +export const ELASTIC_UI_NUMERIC_FONT_FAMILY = "'Elastic UI Numeric'"; + +type MutableRecord = Record; + +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); +}