+
{renderWithProps(datum.icon, {
width: sizes.iconSize,
height: sizes.iconSize,
- color: highContrastTextColor,
+ color: colors.highContrast,
})}
)}
-
+
-
-
- {visibility.extra && (
-
- {renderWithProps(extra, { fontSize: sizes.extraFontSize, color: highContrastTextColor })}
-
- )}
-
+ {/* Extra Block */}
+
+ {visibility.extra && extraElement}
+
-
+ {/* Value Block */}
+
+
text).join('')}
+ >
+ {textParts.map(({ emphasis, text }, i) => {
+ return emphasis === 'small' ? (
+
+ {text}
+
+ ) : (
+ text
+ );
+ })}
+
+ {datum.valueIcon && (
text).join('')}
>
- {textParts.map(({ emphasis, text }, i) => {
- return emphasis === 'small' ? (
-
- {text}
-
- ) : (
- text
- );
+ {renderWithProps(datum.valueIcon, {
+ width: sizes.valuePartFontSize,
+ height: sizes.valuePartFontSize,
+ color: datum.valueColor ?? colors.highContrast,
+ verticalAlign: 'middle',
})}
- {datum.valueIcon && (
-
- {renderWithProps(datum.valueIcon, {
- width: sizes.valuePartFontSize,
- height: sizes.valuePartFontSize,
- color: datum.valueColor ?? highContrastTextColor,
- verticalAlign: 'middle',
- })}
-
- )}
-
+ )}
);
diff --git a/packages/charts/src/chart_types/metric/renderer/dom/text_measurements.tsx b/packages/charts/src/chart_types/metric/renderer/dom/text_measurements.tsx
index 75605308bb6..ae91616e10f 100644
--- a/packages/charts/src/chart_types/metric/renderer/dom/text_measurements.tsx
+++ b/packages/charts/src/chart_types/metric/renderer/dom/text_measurements.tsx
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { BADGE_BORDER } from './badge';
import type { TextParts } from './text_processing';
import { getTextParts } from './text_processing';
import { DEFAULT_FONT_FAMILY } from '../../../../common/default_theme_attributes';
@@ -19,13 +20,15 @@ import type { MetricStyle } from '../../../../utils/themes/theme';
import type { MetricDatum, MetricWNumber } from '../../specs';
import { isMetricWProgress } from '../../specs';
-interface HeightBasedSizes {
+/** @internal */
+export interface HeightBasedSizes {
iconSize: number;
titleFontSize: number;
subtitleFontSize: number;
extraFontSize: number;
valueFontSize: number;
valuePartFontSize: number;
+ progressBarThickness: number;
}
type BreakPoint = 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl';
@@ -41,11 +44,16 @@ type ElementVisibility = {
/** @internal */
export const PADDING = 8;
-const PROGRESS_BAR_WIDTH = 10; // synced with scss variables
-const PROGRESS_BAR_TARGET_WIDTH = 4;
+/**
+ * Synced with _index.scss
+ * @internal
+ */
+export const PROGRESS_BAR_TARGET_SIZE = 8;
const LINE_HEIGHT = 1.2; // aligned with our CSS
const HEIGHT_BP: [number, number, BreakPoint][] = [
- [100, 200, 'xs'],
+ [0, 100, 'xxxs'],
+ [100, 150, 'xxs'],
+ [150, 200, 'xs'],
[200, 300, 's'],
[300, 400, 'm'],
[400, 500, 'l'],
@@ -97,6 +105,7 @@ const SUBTITLE_FONT: Font = {
...TITLE_FONT,
fontWeight: 'normal',
};
+const PROGRESS_BAR_THICKNESS: Record
= { xxxs: 4, xxs: 4, xs: 8, s: 8, m: 8, l: 8, xl: 8, xxl: 16 };
/**
* Approximate font size to fit given available space
@@ -125,18 +134,30 @@ export function getFitValueFontSize(
return Math.floor(Math.max(Math.min(heightConstrainedSize, widthConstrainedSize), minValueFontSize));
}
+/** @internal */
+export interface Visibility extends ElementVisibility {
+ titleLines: string[];
+ subtitleLines: string[];
+ gapHeight: number;
+}
+
/** @internal */
export interface MetricTextDimensions {
heightBasedSizes: HeightBasedSizes;
hasProgressBar: boolean;
progressBarDirection: LayoutDirection | undefined;
+ /**
+ * This only applies when there is a progress bar and is vertical.
+ * We have added the padding into the calculation
+ */
progressBarWidth: number;
- visibility: ElementVisibility & {
- titleLines: string[];
- subtitleLines: string[];
- gapHeight: number;
- };
+ visibility: Visibility;
textParts: TextParts[];
+ /**
+ * This only applies when there is an icon and the value postition is top.
+ * We have added the padding into the calculation
+ */
+ iconGridColumnWidth: number;
}
/** @internal */
@@ -146,21 +167,42 @@ export function getMetricTextPartDimensions(
style: MetricStyle,
locale: string,
): MetricTextDimensions {
- const heightBasedSizes = getHeightBasedFontSizes(HEIGHT_BP, panel.height, style);
+ const heightBasedSizes = getHeightBasedFontSizes(HEIGHT_BP, Math.min(panel.height, panel.width), style);
const hasProgressBar = isMetricWProgress(datum);
const hasTarget = !isNil((datum as MetricWNumber)?.target);
const progressBarDirection = isMetricWProgress(datum) ? datum.progressBarDirection : undefined;
+ const hasVerticalProgressBar = hasProgressBar && progressBarDirection === LayoutDirection.Vertical;
+ const hasHorizontalProgressBar = hasProgressBar && progressBarDirection === LayoutDirection.Horizontal;
+
+ const { progressBarThickness, iconSize } = heightBasedSizes;
+
+ const progressBarTotalSpace = progressBarThickness + (hasTarget ? PROGRESS_BAR_TARGET_SIZE : 0) + PADDING;
+ const progressBarWidth = hasVerticalProgressBar ? progressBarTotalSpace : 0;
+ const progressBarHeight = hasHorizontalProgressBar ? progressBarTotalSpace : 0;
+
+ const isIconVisible = !!datum.icon && style.valuePosition === 'top';
+ // The width of the icon column, including padding
+ const iconColumnWidth = iconSize + PADDING;
+ // If the value is center-aligned and the icon is visible, add an extra column width for visual centering
+ const needsCenterSpacer = isIconVisible && style.valueTextAlign === 'center';
+ const iconGridColumnWidth = isIconVisible ? iconColumnWidth * (needsCenterSpacer ? 2 : 1) : 0;
+
return {
heightBasedSizes,
hasProgressBar,
progressBarDirection,
- progressBarWidth:
- hasProgressBar && progressBarDirection === LayoutDirection.Vertical
- ? PROGRESS_BAR_WIDTH + (hasTarget ? PROGRESS_BAR_TARGET_WIDTH : 0)
- : 0,
- visibility: elementVisibility(datum, panel, heightBasedSizes, locale, style.valueFontSize === 'fit'),
+ progressBarWidth,
+ visibility: elementVisibility(
+ datum,
+ panel,
+ heightBasedSizes,
+ locale,
+ style.valueFontSize === 'fit',
+ progressBarHeight,
+ ),
textParts: getTextParts(datum, style),
+ iconGridColumnWidth,
};
}
@@ -182,6 +224,7 @@ function getHeightBasedFontSizes(
extraFontSize: EXTRA_FONT_SIZE[size],
valueFontSize,
valuePartFontSize,
+ progressBarThickness: PROGRESS_BAR_THICKNESS[size],
};
}
@@ -223,13 +266,23 @@ export function getSnappedFontSizes(
};
}
-/** @internal */
-export function elementVisibility(
+const getResponsiveBreakpoints = (title: boolean, subtitle: boolean, extra: boolean): Array => [
+ { titleMaxLines: 3, subtitleMaxLines: 2, title, subtitle, extra },
+ { titleMaxLines: 3, subtitleMaxLines: 1, title, subtitle, extra },
+ { titleMaxLines: 2, subtitleMaxLines: 1, title, subtitle, extra },
+ { titleMaxLines: 1, subtitleMaxLines: 1, title, subtitle, extra },
+ { titleMaxLines: 1, subtitleMaxLines: 0, title, subtitle: false, extra },
+ { titleMaxLines: 1, subtitleMaxLines: 0, title, subtitle: false, extra: false },
+ { titleMaxLines: 1, subtitleMaxLines: 0, title, subtitle: false, extra: false },
+];
+
+function elementVisibility(
datum: MetricDatum,
panel: Size,
sizes: HeightBasedSizes,
locale: string,
fit: boolean,
+ progressBarHeight: number,
): ElementVisibility & {
titleLines: string[];
subtitleLines: string[];
@@ -255,18 +308,16 @@ export function elementVisibility(
: 0;
};
- const extraHeight = sizes.extraFontSize * LINE_HEIGHT;
+ // If there is a badge, we add the padding to the extra height
+ const hasBadge = !!(datum?.extra && 'badgeColor' in datum?.extra && datum?.extra?.badgeColor);
+ const badgeHeight = hasBadge ? BADGE_BORDER * 2 : 0;
+
+ // We assume that the extra element is taking one line
+ const extraHeight = sizes.extraFontSize * LINE_HEIGHT + badgeHeight;
+
const valueHeight = sizes.valueFontSize * LINE_HEIGHT;
- const responsiveBreakPoints: Array = [
- { titleMaxLines: 3, subtitleMaxLines: 2, title: !!datum.title, subtitle: !!datum.subtitle, extra: !!datum.extra },
- { titleMaxLines: 3, subtitleMaxLines: 1, title: !!datum.title, subtitle: !!datum.subtitle, extra: !!datum.extra },
- { titleMaxLines: 2, subtitleMaxLines: 1, title: !!datum.title, subtitle: !!datum.subtitle, extra: !!datum.extra },
- { titleMaxLines: 1, subtitleMaxLines: 1, title: !!datum.title, subtitle: !!datum.subtitle, extra: !!datum.extra },
- { titleMaxLines: 1, subtitleMaxLines: 0, title: !!datum.title, subtitle: false, extra: !!datum.extra },
- { titleMaxLines: 1, subtitleMaxLines: 0, title: !!datum.title, subtitle: false, extra: false },
- { titleMaxLines: 1, subtitleMaxLines: 0, title: !!datum.title, subtitle: false, extra: false },
- ];
+ const responsiveBreakPoints = getResponsiveBreakpoints(!!datum.title, !!datum.subtitle, !!datum.extra);
const getCombinedHeight = (
{ titleMaxLines, subtitleMaxLines, title, subtitle, extra }: ElementVisibility,
@@ -276,14 +327,25 @@ export function elementVisibility(
(subtitle && subtitleMaxLines > 0 ? subtitleHeight(subtitleMaxLines, measure) : 0) +
(extra ? extraHeight : 0) +
valueHeight +
- PADDING;
+ PADDING +
+ // For the height calculation, take into account when there is an horizontal progress bar
+ progressBarHeight;
+ /**
+ * Determines if the given breakpoint should be considered "visible"
+ * for the provided text measurement.
+ */
const isVisible = (ev: ElementVisibility, measure: TextMeasure) => getCombinedHeight(ev, measure) < panel.height;
return withTextMeasure((textMeasure) => {
- const visibilityBreakpoint = fit
- ? responsiveBreakPoints.at(0)!
- : responsiveBreakPoints.find((breakpoint) => isVisible(breakpoint, textMeasure)) ?? responsiveBreakPoints.at(-1)!;
+ let visibilityBreakpoint: ElementVisibility;
+
+ if (fit) {
+ visibilityBreakpoint = responsiveBreakPoints.at(0)!;
+ } else {
+ const found = responsiveBreakPoints.find((breakpoint) => isVisible(breakpoint, textMeasure));
+ visibilityBreakpoint = found ?? responsiveBreakPoints.at(-1)!;
+ }
return {
...visibilityBreakpoint,
diff --git a/packages/charts/src/chart_types/metric/renderer/dom/titles.tsx b/packages/charts/src/chart_types/metric/renderer/dom/titles.tsx
new file mode 100644
index 00000000000..6a5394eab55
--- /dev/null
+++ b/packages/charts/src/chart_types/metric/renderer/dom/titles.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 classNames from 'classnames';
+import type { CSSProperties } from 'react';
+import React, { useMemo } from 'react';
+
+import type { HeightBasedSizes, Visibility } from './text_measurements';
+import type { Color } from '../../../../common/colors';
+import type { MetricFontWeight, MetricStyle } from '../../../../utils/themes/theme';
+
+function lineClamp(maxLines: number): CSSProperties {
+ return {
+ textOverflow: 'ellipsis',
+ display: '-webkit-box',
+ WebkitLineClamp: maxLines, // due to an issue with react CSSProperties filtering out this line, see https://github.com/facebook/react/issues/23033
+ lineClamp: maxLines,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ whiteSpace: 'pre-wrap',
+ };
+}
+
+interface TitleProps {
+ metricId: string;
+ title: string | undefined;
+ titleWeight: MetricFontWeight;
+ fontSize: number;
+ textAlign: MetricStyle['titlesTextAlign'];
+ linesLength: number;
+ onElementClick?: () => void;
+}
+
+const Title: React.FC = ({
+ metricId,
+ title,
+ titleWeight,
+ fontSize,
+ textAlign,
+ linesLength,
+ onElementClick,
+}) => {
+ const contentStyle = {
+ fontSize,
+ textAlign,
+ fontWeight: titleWeight,
+ ...lineClamp(linesLength),
+ };
+
+ const content = (
+
+ {title}
+
+ );
+
+ return (
+
+ {onElementClick ? (
+
+ ) : (
+ content
+ )}
+
+ );
+};
+
+interface SubtitleProps {
+ subtitle: string | undefined;
+ fontSize: number;
+ subtitleLines: string[];
+ color: Color;
+}
+
+const Subtitle: React.FC = ({ subtitle, fontSize, subtitleLines, color }) => {
+ return (
+
+ {subtitle}
+
+ );
+};
+
+interface TitlesBlockProps {
+ // Identification & text
+ metricId: string;
+ title: string | undefined;
+ subtitle: string | undefined;
+
+ // Layout & sizing
+ sizes: HeightBasedSizes;
+ visibility: Visibility;
+
+ // Alignment & icon
+ textAlign: MetricStyle['titlesTextAlign'];
+ titleWeight: MetricFontWeight;
+ isIconVisible: boolean;
+
+ titlesRow: string;
+ titlesColumn: string;
+
+ subtitleColor: Color;
+
+ // Events
+ onElementClick?: () => void;
+}
+
+/** @internal */
+export const TitlesBlock: React.FC = ({
+ metricId,
+ title,
+ subtitle,
+ sizes,
+ visibility,
+ textAlign,
+ titleWeight,
+ isIconVisible,
+ titlesRow,
+ titlesColumn,
+ subtitleColor,
+ onElementClick,
+}) => {
+ const { titleFontSize, subtitleFontSize } = sizes;
+ const { title: showTitle, subtitle: showSubtitle, titleLines, subtitleLines } = visibility;
+
+ const titlesBlockStyle = useMemo(() => {
+ if (!isIconVisible) return undefined;
+ return {
+ gridRow: titlesRow,
+ gridColumn: titlesColumn,
+ };
+ }, [isIconVisible, titlesRow, titlesColumn]);
+
+ return (
+
+ {showTitle && (
+
+ )}
+
+ {showSubtitle && (
+
+ )}
+
+ );
+};
diff --git a/packages/charts/src/chart_types/metric/specs/index.ts b/packages/charts/src/chart_types/metric/specs/index.ts
index d0e724f74e8..f17b52da1d3 100644
--- a/packages/charts/src/chart_types/metric/specs/index.ts
+++ b/packages/charts/src/chart_types/metric/specs/index.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import type { ComponentProps, ComponentType, ReactElement, ReactNode } from 'react';
+import type { ComponentProps, ComponentType, CSSProperties, ReactElement, ReactNode } from 'react';
import type { $Values } from 'utility-types';
import { ChartType } from '../..';
@@ -18,6 +18,32 @@ import type { LayoutDirection, ValueFormatter } from '../../../utils/common';
import type { GenericDomain } from '../../../utils/domain';
import type { BulletValueLabels } from '../../bullet_graph/spec';
+/**
+ * Props for displaying a secondary metric value with optional label and badge styling.
+ *
+ * @alpha
+ * */
+export interface SecondaryMetricProps {
+ /** The main value to display */
+ value: string;
+ /** Optional label to display alongside the value */
+ label?: string;
+ /** Optional background color for the value badge. If not provided, no badge is displayed */
+ badgeColor?: Color;
+ /** Determines whether the label appears before or after the value */
+ labelPosition?: 'before' | 'after';
+ /** Optional CSS properties to apply to the container element */
+ style?: CSSProperties;
+ /** Optional aria description */
+ ariaDescription?: string;
+ /** Border color applied to the badge */
+ badgeBorderColor?: Color;
+ /** Optional icon displayed next to the text */
+ icon?: string;
+ /** Determines whether the icon appears before or after the value */
+ iconPosition?: 'before' | 'after';
+}
+
/** @alpha */
export type MetricBase = {
color: Color;
@@ -29,7 +55,12 @@ export type MetricBase = {
valueColor?: Color;
valueIcon?: ComponentType<{ width: number; height: number; color: Color; verticalAlign: 'middle' }>;
subtitle?: string;
- extra?: ReactElement | ComponentType<{ fontSize: number; color: string }>;
+ /**
+ * Optional extra content to display below the subtitle.
+ * - Can be a React element, a component type (which will receive `fontSize` and `color` props), or a SecondaryMetricProps object.
+ * - If a SecondaryMetricProps object is provided, it will be rendered using the SecondaryMetric component.
+ */
+ extra?: ReactElement | ComponentType<{ fontSize: number; color: string }> | SecondaryMetricProps;
icon?: ComponentType<{ width: number; height: number; color: Color }>;
body?: ReactNode;
};
@@ -179,3 +210,14 @@ export function isMetricWTrend(datum: MetricDatum): datum is MetricWTrend {
!datum.hasOwnProperty('domainMax')
);
}
+
+/** @internal */
+export const isSecondaryMetricProps = (props: any): props is SecondaryMetricProps => {
+ return (
+ props &&
+ 'value' in props &&
+ typeof props.value === 'string' &&
+ (props.label === undefined || typeof props.label === 'string') &&
+ (props.badgeColor === undefined || typeof props.badgeColor === 'string')
+ );
+};
diff --git a/packages/charts/src/chart_types/specs.ts b/packages/charts/src/chart_types/specs.ts
index 4565144a2c0..17d5cd54d79 100644
--- a/packages/charts/src/chart_types/specs.ts
+++ b/packages/charts/src/chart_types/specs.ts
@@ -48,6 +48,7 @@ export {
MetricWTrend,
MetricTrendShape,
MetricDatum,
+ SecondaryMetricProps,
} from './metric/specs';
export { Bullet, BulletProps, BulletSpec, BulletDatum, BulletSubtype, BulletValueLabels } from './bullet_graph/spec';
diff --git a/packages/charts/src/common/color_calcs.ts b/packages/charts/src/common/color_calcs.ts
index 0a9cb9fa838..8ced4809f35 100644
--- a/packages/charts/src/common/color_calcs.ts
+++ b/packages/charts/src/common/color_calcs.ts
@@ -10,7 +10,7 @@ import type { Required } from 'utility-types';
import { APCAContrast } from './apca_color_contrast';
import type { RgbaTuple, RgbTuple } from './color_library_wrappers';
-import { RGBATupleToString } from './color_library_wrappers';
+import { colorToRgba, RGBATupleToString } from './color_library_wrappers';
import type { ColorDefinition } from './colors';
import { Colors } from './colors';
import { getWCAG2ContrastRatio } from './wcag2_color_contrast';
@@ -131,3 +131,76 @@ export function highContrastColor(
): HighContrastResult {
return HIGH_CONTRAST_FN[mode](background, options);
}
+
+/**
+ * Computes the contrast ratio between two colors and determines if a border is needed for sufficient visual separation.
+ * If the contrast is below the specified threshold, recommends a border color that has high contrast with both input colors.
+ *
+ * @param colorA - The first color to compare (can be background, foreground, or any color).
+ * @param colorB - The second color to compare.
+ * @param options - Optional settings for contrast mode, threshold, and border color preferences.
+ *
+ * @internal
+ */
+export function getContrastRecommendation(
+ colorA: string,
+ colorB: string,
+ options: {
+ contrastMode?: 'WCAG2' | 'WCAG3';
+ contrastThreshold?: number;
+ borderOptions?: ColorContrastOptions;
+ } = {},
+) {
+ const {
+ contrastMode = 'WCAG2',
+ contrastThreshold = 4.5, // WCAG AA standard
+ borderOptions,
+ } = options;
+
+ const rgbA = colorToRgba(colorA).slice(0, 3) as RgbTuple;
+ const rgbB = colorToRgba(colorB).slice(0, 3) as RgbTuple;
+
+ const contrastRatio =
+ contrastMode === 'WCAG2' ? getWCAG2ContrastRatio(rgbA, rgbB) : Math.abs(APCAContrast(rgbA, rgbB));
+
+ // If contrast is sufficient, no border is needed
+ if (contrastRatio >= contrastThreshold) {
+ return {
+ needsBorder: false,
+ contrastRatio,
+ borderColor: undefined,
+ shade: undefined,
+ color1Contrast: undefined,
+ color2Contrast: undefined,
+ };
+ }
+
+ // Get the optimal border color (high contrast against both colors)
+ const color1Result = highContrastColor(rgbA, contrastMode, borderOptions);
+ const color2Result = highContrastColor(rgbB, contrastMode, borderOptions);
+
+ const selectedShade =
+ color1Result.shade === color2Result.shade
+ ? color1Result.shade // If both prefer the same shade, use it
+ : color1Result.ratio > color2Result.ratio // Otherwise use the one with better overall contrast
+ ? color1Result.shade
+ : color2Result.shade;
+
+ // Use the color that works best with both backgrounds
+ // This prefers borders that have sufficient contrast with both colors
+ const borderColor =
+ color1Result.shade === color2Result.shade
+ ? color1Result.color.keyword // If both prefer the same shade, use it
+ : color1Result.ratio > color2Result.ratio // Otherwise use the one with better overall contrast
+ ? color1Result.color.keyword
+ : color2Result.color.keyword;
+
+ return {
+ needsBorder: true,
+ contrastRatio,
+ borderColor,
+ shade: selectedShade,
+ color1Contrast: color1Result.ratio,
+ color2Contrast: color2Result.ratio,
+ };
+}
diff --git a/packages/charts/src/common/text_utils.ts b/packages/charts/src/common/text_utils.ts
index a1bd046fa7a..98f8904163f 100644
--- a/packages/charts/src/common/text_utils.ts
+++ b/packages/charts/src/common/text_utils.ts
@@ -15,8 +15,8 @@ import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb
import type { TextMeasure } from '../utils/bbox/canvas_text_bbox_calculator';
import type { Datum } from '../utils/common';
-const FONT_WEIGHTS_NUMERIC = [100, 200, 300, 400, 500, 600, 700, 800, 900];
-const FONT_WEIGHTS_ALPHA = ['normal', 'bold', 'lighter', 'bolder', 'inherit', 'initial', 'unset'];
+const FONT_WEIGHTS_NUMERIC = [100, 200, 300, 400, 500, 600, 700, 800, 900] as const;
+const FONT_WEIGHTS_ALPHA = ['normal', 'bold', 'lighter', 'bolder', 'inherit', 'initial', 'unset'] as const;
/**
* todo consider doing tighter control for permissible font families, eg. as in Kibana Canvas - expression language
@@ -25,7 +25,7 @@ const FONT_WEIGHTS_ALPHA = ['normal', 'bold', 'lighter', 'bolder', 'inherit', 'i
*/
export type FontFamily = string;
/** @public */
-export const FONT_WEIGHTS = Object.freeze([...FONT_WEIGHTS_NUMERIC, ...FONT_WEIGHTS_ALPHA] as const);
+export const FONT_WEIGHTS = Object.freeze([...FONT_WEIGHTS_NUMERIC, ...FONT_WEIGHTS_ALPHA]);
/** @public */
export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const);
/** @public */
diff --git a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts
index 7228f7f35eb..afa429a3172 100644
--- a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts
+++ b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts
@@ -418,16 +418,23 @@ export const AMSTERDAM_DARK_THEME: Theme = {
metric: {
textLightColor: '#E0E5EE',
textDarkColor: '#343741',
+ textSubtitleLightColor: '#E0E5EE',
+ textSubtitleDarkColor: '#343741',
+ textExtraLightColor: '#E0E5EE',
+ textExtraDarkColor: '#343741',
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: '#343741',
barBackground: '#343741',
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: {
textColor: '#E0E5EE',
diff --git a/packages/charts/src/utils/themes/amsterdam_light_theme.ts b/packages/charts/src/utils/themes/amsterdam_light_theme.ts
index 6ab395e938c..75622ab964c 100644
--- a/packages/charts/src/utils/themes/amsterdam_light_theme.ts
+++ b/packages/charts/src/utils/themes/amsterdam_light_theme.ts
@@ -418,16 +418,23 @@ export const AMSTERDAM_LIGHT_THEME: Theme = {
metric: {
textLightColor: '#E0E5EE',
textDarkColor: '#343741',
+ textSubtitleLightColor: '#E0E5EE',
+ textSubtitleDarkColor: '#343741',
+ textExtraLightColor: '#E0E5EE',
+ textExtraDarkColor: '#343741',
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: '#EDF0F5',
barBackground: '#EDF0F5',
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: {
textColor: '#343741',
diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts
index 3c7a7b5b8f6..ea7cff99edf 100644
--- a/packages/charts/src/utils/themes/dark_theme.ts
+++ b/packages/charts/src/utils/themes/dark_theme.ts
@@ -439,17 +439,24 @@ export const DARK_THEME: Theme = {
},
metric: {
textLightColor: DARK_TEXT_COLORS.textHeading,
+ textSubtitleLightColor: DARK_TEXT_COLORS.textParagraph,
+ textExtraLightColor: DARK_TEXT_COLORS.textParagraph,
textDarkColor: LIGHT_TEXT_COLORS.textHeading,
+ textSubtitleDarkColor: LIGHT_TEXT_COLORS.textParagraph,
+ textExtraDarkColor: LIGHT_TEXT_COLORS.textParagraph,
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: DARK_BORDER_COLORS.borderBaseSubdued,
barBackground: DARK_BACKGROUND_COLORS.backgroundBaseDisabled,
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: DARK_THEME_BULLET_STYLE,
tooltip: {
diff --git a/packages/charts/src/utils/themes/legacy_dark_theme.ts b/packages/charts/src/utils/themes/legacy_dark_theme.ts
index 45c4e85374c..f0fb30a0911 100644
--- a/packages/charts/src/utils/themes/legacy_dark_theme.ts
+++ b/packages/charts/src/utils/themes/legacy_dark_theme.ts
@@ -422,16 +422,23 @@ export const LEGACY_DARK_THEME: Theme = {
metric: {
textLightColor: '#E0E5EE',
textDarkColor: '#343741',
+ textSubtitleLightColor: '#E0E5EE',
+ textSubtitleDarkColor: '#343741',
+ textExtraLightColor: '#E0E5EE',
+ textExtraDarkColor: '#343741',
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: '#343741',
barBackground: '#343741',
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: DARK_THEME_BULLET_STYLE,
tooltip: {
diff --git a/packages/charts/src/utils/themes/legacy_light_theme.ts b/packages/charts/src/utils/themes/legacy_light_theme.ts
index 214f96a88d2..396c205ccd9 100644
--- a/packages/charts/src/utils/themes/legacy_light_theme.ts
+++ b/packages/charts/src/utils/themes/legacy_light_theme.ts
@@ -420,17 +420,24 @@ export const LEGACY_LIGHT_THEME: Theme = {
},
metric: {
textLightColor: '#E0E5EE',
+ textSubtitleLightColor: '#E0E5EE',
+ textSubtitleDarkColor: '#343741',
+ textExtraLightColor: '#E0E5EE',
+ textExtraDarkColor: '#343741',
textDarkColor: '#343741',
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: '#EDF0F5',
barBackground: '#EDF0F5',
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: LIGHT_THEME_BULLET_STYLE,
tooltip: {
diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts
index f15574dcc6d..f55d106687b 100644
--- a/packages/charts/src/utils/themes/light_theme.ts
+++ b/packages/charts/src/utils/themes/light_theme.ts
@@ -437,17 +437,24 @@ export const LIGHT_THEME: Theme = {
},
metric: {
textLightColor: DARK_TEXT_COLORS.textHeading,
+ textSubtitleLightColor: DARK_TEXT_COLORS.textParagraph,
+ textExtraLightColor: DARK_TEXT_COLORS.textParagraph,
textDarkColor: LIGHT_TEXT_COLORS.textHeading,
+ textSubtitleDarkColor: LIGHT_TEXT_COLORS.textParagraph,
+ textExtraDarkColor: LIGHT_TEXT_COLORS.textParagraph,
valueFontSize: 'default',
minValueFontSize: 12,
titlesTextAlign: 'left',
- valuesTextAlign: 'right',
+ extraTextAlign: 'right',
+ valueTextAlign: 'right',
+ valuePosition: 'bottom',
iconAlign: 'right',
border: LIGHT_BORDER_COLORS.borderBaseSubdued,
barBackground: LIGHT_BACKGROUND_COLORS.backgroundBaseDisabled,
emptyBackground: Colors.Transparent.keyword,
nonFiniteText: 'N/A',
minHeight: 64,
+ titleWeight: 'bold',
},
bulletGraph: LIGHT_THEME_BULLET_STYLE,
tooltip: {
diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts
index 63244cb7492..86a2040a3d9 100644
--- a/packages/charts/src/utils/themes/theme.ts
+++ b/packages/charts/src/utils/themes/theme.ts
@@ -13,7 +13,7 @@ import type { PartitionStyle } from './partition';
import type { BulletStyle } from '../../chart_types/bullet_graph/theme';
import type { Color } from '../../common/colors';
import type { Pixels, Radian, Ratio } from '../../common/geometry';
-import type { Font, FontStyle } from '../../common/text_utils';
+import type { Font, FontStyle, FontWeight, TextAlign } from '../../common/text_utils';
import type { ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment } from '../common';
import type { Margins, Padding, SimplePadding } from '../dimensions';
import type { Point } from '../point';
@@ -193,9 +193,7 @@ export interface GridLineStyle {
dash: number[];
}
-/**
- * @public
- */
+/** @public */
export interface GoalStyles {
progressLine: Pick;
targetLine: Pick;
@@ -244,9 +242,7 @@ export interface GoalStyles {
capturePad: number;
}
-/**
- * @public
- */
+/** @public */
export interface HeatmapStyle {
/**
* Config of the mask over the area outside of the selected cells
@@ -305,15 +301,36 @@ export interface HeatmapStyle {
maxLegendHeight?: number;
}
-/** @public */
+/**
+ * Metric font weight options for text styling.
+ * @public
+ */
+export type MetricFontWeight = Extract;
+
+/**
+ * Style options for the Metric chart type.
+ * @public
+ */
export interface MetricStyle {
textDarkColor: Color;
textLightColor: Color;
+ textSubtitleDarkColor: Color;
+ textSubtitleLightColor: Color;
+ textExtraDarkColor: Color;
+ textExtraLightColor: Color;
+
valueFontSize: 'default' | 'fit' | number;
minValueFontSize: number;
- titlesTextAlign: 'left' | 'center' | 'right';
- valuesTextAlign: 'left' | 'center' | 'right';
- iconAlign: 'left' | 'right';
+
+ // Alignments
+ titlesTextAlign: Extract;
+ extraTextAlign: Extract;
+ valueTextAlign: Extract;
+ valuePosition: 'top' | 'bottom';
+ iconAlign: Extract;
+
+ titleWeight: MetricFontWeight;
+
border: Color;
barBackground: Color;
emptyBackground: Color;
diff --git a/storybook/stories/metric/1_basic.story.tsx b/storybook/stories/metric/1_basic.story.tsx
index 1e23714d7bf..69b9c677edc 100644
--- a/storybook/stories/metric/1_basic.story.tsx
+++ b/storybook/stories/metric/1_basic.story.tsx
@@ -19,17 +19,12 @@ import type { ChartsStory } from '../../types';
import { useBaseTheme } from '../../use_base_theme';
import { customKnobs } from '../utils/knobs';
-type TextAlign = 'left' | 'center' | 'right';
-const getTextAlignKnob = (name: string, defaultValue: TextAlign): TextAlign =>
- select(
- name,
- {
- Left: 'left',
- Center: 'center',
- Right: 'right',
- },
- defaultValue,
- );
+const getTextAlignKnob = (
+ name: string,
+ defaultValue: 'left' | 'center' | 'right',
+ groupId?: string,
+): 'left' | 'center' | 'right' =>
+ select(name, { Left: 'left', Center: 'center', Right: 'right' }, defaultValue, groupId);
export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
const title = text('title', '21d7f8b7-92ea-41a0-8c03-0db0ec7e11b9');
@@ -84,7 +79,9 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
);
const valueFontSize = number('value font size (px)', 40, { min: 0, step: 10 });
const titlesTextAlign = getTextAlignKnob('title text-align', 'left');
- const valuesTextAlign = getTextAlignKnob('values text-align', 'right');
+ const valueTextAlign = getTextAlignKnob('value text-align', 'right');
+ const extraTextAlign = getTextAlignKnob('extra text-align', 'right');
+ const valuePosition = select('value position', { Bottom: 'bottom', Top: 'top' }, 'bottom');
const iconAlign = select(
'icon align',
{
@@ -108,6 +105,8 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
...(showValueIcon ? { valueIcon: getIcon(valueIconType) } : {}),
};
+ const target = number('target', 75, { range: true, min: -200, max: 200 });
+
const numericData: MetricWProgress | MetricWNumber | MetricWTrend = {
...data,
value: Number.parseFloat(value),
@@ -121,6 +120,7 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
trendA11yDescription,
}
: {}),
+ target,
};
const textualData: MetricWText | MetricWTrend = {
...data,
@@ -141,6 +141,7 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
const onEventOutAction = action('out');
const configuredData = [[numberTextSwitch ? numericData : textualData]];
+
return (
{
blendingBackground: useBlendingBackground ? blendingBackground : undefined,
valueFontSize: valueFontSizeMode === 'custom' ? valueFontSize : valueFontSizeMode,
titlesTextAlign,
- valuesTextAlign,
+ valueTextAlign,
+ extraTextAlign,
iconAlign,
+ valuePosition,
},
}}
baseTheme={useBaseTheme()}
diff --git a/storybook/stories/metric/6_extra_badge.story.tsx b/storybook/stories/metric/6_extra_badge.story.tsx
index e13faf1d345..3335f31c82b 100644
--- a/storybook/stories/metric/6_extra_badge.story.tsx
+++ b/storybook/stories/metric/6_extra_badge.story.tsx
@@ -80,7 +80,8 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
);
const valueFontSize = number('value font size (px)', 40, { min: 0, step: 10 });
const titlesTextAlign = 'left';
- const valuesTextAlign = 'right';
+ const valueTextAlign = 'right';
+ const extraTextAlign = 'right';
const iconAlign = 'right';
const getIcon =
(type: string) =>
@@ -154,7 +155,8 @@ export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
blendingBackground: useBlendingBackground ? blendingBackground : undefined,
valueFontSize: valueFontSizeMode === 'custom' ? valueFontSize : valueFontSizeMode,
titlesTextAlign,
- valuesTextAlign,
+ valueTextAlign,
+ extraTextAlign,
iconAlign,
},
}}
diff --git a/storybook/stories/metric/7_layout.story.tsx b/storybook/stories/metric/7_layout.story.tsx
new file mode 100644
index 00000000000..7763accd08a
--- /dev/null
+++ b/storybook/stories/metric/7_layout.story.tsx
@@ -0,0 +1,280 @@
+/*
+ * 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 { EuiIcon } from '@elastic/eui';
+import { action } from '@storybook/addon-actions';
+import { select, boolean, text, color, number } from '@storybook/addon-knobs';
+import React from 'react';
+
+import type { MetricWProgress, MetricWTrend, MetricWText, MetricWNumber, SecondaryMetricProps } from '@elastic/charts';
+import { Chart, isMetricElementEvent, Metric, MetricTrendShape, Settings } from '@elastic/charts';
+import { KIBANA_METRICS } from '@elastic/charts/src/utils/data_samples/test_dataset_kibana';
+
+import type { ChartsStory } from '../../types';
+import { useBaseTheme } from '../../use_base_theme';
+import { customKnobs } from '../utils/knobs';
+
+const getTextAlignKnob = (
+ name: string,
+ defaultValue: 'left' | 'center' | 'right',
+ groupId?: string,
+): 'left' | 'center' | 'right' =>
+ select(name, { Left: 'left', Center: 'center', Right: 'right' }, defaultValue, groupId);
+
+const getIcon =
+ (type: string) =>
+ ({ width, height, color }: { width: number; height: number; color: string }) => (
+
+ );
+
+const textConfigurationAndPositionGroup = 'Text configuration and position';
+const secondaryMetricGroup = 'Secondary metric';
+const colorGroup = 'Colors';
+
+export const Example: ChartsStory = (_, { title: storyTitle, description }) => {
+ // Title and subtitle values
+ const title = text('Title', 'Count of records');
+ const subtitle = text('Subtitle', 'Little description of this component');
+
+ // Visualization type
+ const progressOrTrend = select('Visualization type', { trend: 'trend', bar: 'bar', none: 'none' }, 'bar');
+ const progressBarDirection = select(
+ 'Progress bar direction',
+ { horizontal: 'horizontal', vertical: 'vertical' },
+ 'vertical',
+ );
+
+ const maxDataPoints = number('Trend data points', 30, { min: 0, max: 50, step: 1 });
+ const trendShape = customKnobs.fromEnum('Trend shape', MetricTrendShape, MetricTrendShape.Area);
+ const trendA11yTitle = text('Trend a11y title', 'The Cluster CPU Usage trend');
+ const trendA11yDescription = text(
+ 'Trend a11y description',
+ 'The trend shows a peak of CPU usage in the last 5 minutes',
+ );
+
+ const progressMax = number('Progress max', 100);
+ const numberTextSwitch = boolean('Is numeric metric', true);
+ const value = text('Value', '55.23');
+ const valuePrefix = text('Value prefix', '');
+ const valuePostfix = text('Value postfix', ' %');
+
+ // Secondary metric
+
+ const showExtra = boolean('Show extra', true, secondaryMetricGroup);
+ const useCustomExtra = boolean('Use custom extra', false, secondaryMetricGroup);
+
+ let extra = text('Custom extra', 'last 5m', secondaryMetricGroup);
+ extra = extra.replace('<b>', '');
+ extra = extra.replace('</b>', '');
+
+ const secondaryMetricValue = text('Secondary metric value', '87.20', secondaryMetricGroup);
+ const secondaryMetricIcon = select(
+ 'Secondary metric trend icon',
+ { Increase: '↑', Decrease: '↓', Stable: '=', Empty: '' },
+ '↑',
+ secondaryMetricGroup,
+ );
+ const secondaryMetricIconPosition = select(
+ 'Secondary metric trend icon position',
+ { Before: 'before', After: 'after' },
+ 'after',
+ secondaryMetricGroup,
+ );
+ const label = text('Label', 'Last week', secondaryMetricGroup);
+ const colorByValue = boolean('Color by value', true, secondaryMetricGroup);
+ const badgeColor = color('Secondary metric value color', 'rgb(93, 191, 149)', secondaryMetricGroup);
+ const secondaryMetricLabelPosition = select(
+ 'Secondary metric label position',
+ { before: 'before', after: 'after' },
+ 'before',
+ secondaryMetricGroup,
+ );
+ const secondaryMetricAriaDescription = text('Aria description', 'This is a description', secondaryMetricGroup);
+
+ const secondaryProps: SecondaryMetricProps = {
+ value: secondaryMetricValue,
+ label,
+ badgeColor: colorByValue ? badgeColor : undefined,
+ labelPosition: secondaryMetricLabelPosition,
+ ariaDescription: secondaryMetricAriaDescription,
+ icon: secondaryMetricIcon,
+ iconPosition: secondaryMetricIconPosition,
+ };
+
+ const dataExtra = showExtra
+ ? useCustomExtra
+ ? { extra: }
+ : { extra: secondaryProps }
+ : {};
+
+ // Colors
+
+ const metricColor = color('Metric color', 'rgb(246, 217, 143)', colorGroup);
+
+ const useValueColor = boolean('Use value color', false, colorGroup);
+ const valueColor = select(
+ 'Value color',
+ {
+ euiColorVisText0: '#065B58',
+ euiColorVisText1: '#047471',
+ euiColorVisText2: '#154399',
+ euiColorVisText3: '#0B64DD',
+ euiColorVisText4: '#A11262',
+ euiColorVisText5: '#D13680',
+ euiColorVisText6: '#A71627',
+ euiColorVisText7: '#DA3737',
+ euiColorVisText8: '#6A4906',
+ euiColorVisText9: '#966B03',
+ },
+ '#065B58',
+ colorGroup,
+ );
+ const useBlendingBackground = boolean('Use blending background', false, colorGroup);
+ const blendingBackground = color('Blending background', 'rgba(255,255,255,1)', colorGroup);
+ const barBackground = color('Bar background', 'rgb(194, 201, 214) ', colorGroup);
+
+ // Metric icon
+ const iconType = 'warning';
+ const showIcon = boolean('Show metric icon', false, textConfigurationAndPositionGroup);
+ const iconAlign = select(
+ 'Metric icon align',
+ { Left: 'left', Right: 'right' },
+ 'right',
+ textConfigurationAndPositionGroup,
+ );
+
+ // Value icon
+ const showValueIcon = boolean('Show value icon', false);
+ const valueIconType = 'sortUp';
+
+ const data = {
+ color: metricColor,
+ title,
+ valueColor: useValueColor ? valueColor : undefined,
+ subtitle,
+ ...dataExtra,
+ ...(showIcon ? { icon: getIcon(iconType) } : {}),
+ ...(showValueIcon ? { valueIcon: getIcon(valueIconType) } : {}),
+ };
+
+ const numericData: MetricWProgress | MetricWNumber | MetricWTrend = {
+ ...data,
+ value: Number.parseFloat(value),
+ valueFormatter: (d: number) => `${valuePrefix}${d}${valuePostfix}`,
+ ...(progressOrTrend === 'bar' ? { domainMax: progressMax, progressBarDirection } : {}),
+ ...(progressOrTrend === 'trend'
+ ? {
+ trend: KIBANA_METRICS.metrics.kibana_os_load.v2.data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })),
+ trendShape,
+ trendA11yTitle,
+ trendA11yDescription,
+ }
+ : {}),
+ };
+
+ const textualData: MetricWText | MetricWTrend = {
+ ...data,
+ value,
+ ...(progressOrTrend === 'bar' ? { domainMax: progressMax, progressBarDirection } : {}),
+ ...(progressOrTrend === 'trend'
+ ? {
+ trend: KIBANA_METRICS.metrics.kibana_os_load.v2.data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })),
+ trendShape,
+ trendA11yTitle,
+ trendA11yDescription,
+ }
+ : {}),
+ };
+
+ const onEventClickAction = action('click');
+ const onEventOverAction = action('over');
+ const onEventOutAction = action('out');
+
+ const configuredData = [[numberTextSwitch ? numericData : textualData]];
+
+ // Configurations
+
+ // Title and subtitle
+ const titlesTextAlign = getTextAlignKnob('Title and subtitle alignment', 'left', textConfigurationAndPositionGroup);
+ const titleWeight = select(
+ 'Title weight',
+ { Bold: 'bold', Normal: 'normal' },
+ 'normal',
+ textConfigurationAndPositionGroup,
+ );
+ // Value (primary metric)
+ const valuePosition = select(
+ 'Primary metric position',
+ { Bottom: 'bottom', Top: 'top' },
+ 'top',
+ textConfigurationAndPositionGroup,
+ );
+ const valueTextAlign = getTextAlignKnob('Primary metric alignment', 'left', textConfigurationAndPositionGroup);
+ const valueFontSizeMode = select(
+ 'Primary metric font size mode',
+ { Default: 'default', Fit: 'fit', Custom: 'custom' },
+ 'default',
+ textConfigurationAndPositionGroup,
+ );
+ const valueFontSize = number(
+ 'Primary metric font size (only if custom font size selected)',
+ 40,
+ { min: 0, step: 10 },
+ textConfigurationAndPositionGroup,
+ );
+ // Extra (secondary metric)
+ const extraTextAlign = getTextAlignKnob('Extra element alignment', 'left', textConfigurationAndPositionGroup);
+
+ const baseTheme = useBaseTheme();
+
+ return (
+
+ {
+ if (isMetricElementEvent(d)) {
+ const { rowIndex, columnIndex } = d;
+ onEventClickAction(
+ `row:${rowIndex} col:${columnIndex} value:${configuredData[rowIndex][columnIndex].value}`,
+ );
+ }
+ }}
+ onElementOver={([d]) => {
+ if (isMetricElementEvent(d)) {
+ const { rowIndex, columnIndex } = d;
+ onEventOverAction(
+ `row:${rowIndex} col:${columnIndex} value:${configuredData[rowIndex][columnIndex].value}`,
+ );
+ }
+ }}
+ onElementOut={() => onEventOutAction('out')}
+ />
+
+
+ );
+};
+
+Example.parameters = {
+ resize: {
+ height: '200px',
+ width: '200px',
+ },
+};
diff --git a/storybook/stories/metric/metric.stories.tsx b/storybook/stories/metric/metric.stories.tsx
index 22f2a14c3bf..0e2868022a2 100644
--- a/storybook/stories/metric/metric.stories.tsx
+++ b/storybook/stories/metric/metric.stories.tsx
@@ -16,3 +16,4 @@ export { Example as bodyContent } from './3_body.story';
export { Example as trendMultipleData } from './4_trend_bug.story';
export { Example as arrayOfValues } from './5_array_of_values.story';
export { Example as extraBadges } from './6_extra_badge.story';
+export { Example as layout } from './7_layout.story';