diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/metric-alpha/grid-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/metric-alpha/grid-chrome-linux.png index 87cdf7ba5f..549e89b81f 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/metric-alpha/grid-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/metric-alpha/grid-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-chrome-linux.png index 79744d500f..d42bbb0653 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-in-dark-mode-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-in-dark-mode-chrome-linux.png index 2ac512218a..5a350ba6dd 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-in-dark-mode-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-horizontal-progress-bar-in-dark-mode-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-chrome-linux.png index 79744d500f..d42bbb0653 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-in-dark-mode-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-in-dark-mode-chrome-linux.png index 2ac512218a..5a350ba6dd 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-in-dark-mode-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-no-progress-bar-in-dark-mode-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-vertical-progress-bar-in-dark-mode-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-vertical-progress-bar-in-dark-mode-chrome-linux.png index 2e924adb7f..ab17293256 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-vertical-progress-bar-in-dark-mode-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-vertical-progress-bar-in-dark-mode-chrome-linux.png differ diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 9e71131fe7..2a66c6dfc1 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -1718,8 +1718,6 @@ export const Metric: FC string; color: Color; title?: string; subtitle?: string; @@ -1731,6 +1729,9 @@ export type MetricBase = { }>; }; +// @alpha (undocumented) +export type MetricDatum = MetricWNumber | MetricWText | MetricWProgress | MetricWTrend; + // @public export type MetricElementEvent = { type: 'metricElementEvent'; @@ -1743,7 +1744,7 @@ export interface MetricSpec extends Spec { // (undocumented) chartType: typeof ChartType.Metric; // (undocumented) - data: (MetricBase | MetricWProgress | MetricWTrend | undefined)[][]; + data: (MetricDatum | undefined)[][]; // (undocumented) specType: typeof SpecType.Series; } @@ -1778,18 +1779,29 @@ export const MetricTrendShape: Readonly<{ export type MetricTrendShape = $Values; // @alpha (undocumented) -export type MetricWProgress = MetricBase & { +export type MetricWNumber = MetricBase & { + value: number; + valueFormatter: (d: number) => string; +}; + +// @alpha (undocumented) +export type MetricWProgress = MetricWNumber & { domainMax: number; - progressBarDirection?: LayoutDirection; + progressBarDirection: LayoutDirection; +}; + +// @alpha (undocumented) +export type MetricWText = MetricBase & { + value: string; }; // @alpha (undocumented) -export type MetricWTrend = MetricBase & { +export type MetricWTrend = MetricWNumber & { trend: { x: number; y: number; }[]; - trendShape?: MetricTrendShape; + trendShape: MetricTrendShape; trendA11yTitle?: string; trendA11yDescription?: string; }; diff --git a/packages/charts/src/chart_types/metric/renderer/dom/_text.scss b/packages/charts/src/chart_types/metric/renderer/dom/_text.scss index 1d1a7e55fe..2b4655482a 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/_text.scss +++ b/packages/charts/src/chart_types/metric/renderer/dom/_text.scss @@ -47,6 +47,7 @@ font-weight: 600; text-align: right; white-space: nowrap; + overflow: hidden; } &__part { diff --git a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx index c037ec2864..d4f9185038 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx @@ -17,7 +17,7 @@ import { BasicListener, ElementClickListener, ElementOverListener, MetricElement import { LayoutDirection } from '../../../../utils/common'; import { Size } from '../../../../utils/dimensions'; import { MetricStyle } from '../../../../utils/themes/theme'; -import { isMetricWProgress, isMetricWTrend, MetricBase, MetricWProgress, MetricWTrend } from '../../specs'; +import { isMetricWProgress, isMetricWTrend, MetricDatum } from '../../specs'; import { ProgressBar } from './progress'; import { SparkLine } from './sparkline'; import { MetricText } from './text'; @@ -29,7 +29,7 @@ export const Metric: React.FunctionComponent<{ columnIndex: number; totalColumns: number; totalRows: number; - datum: MetricBase | MetricWProgress | MetricWTrend; + datum: MetricDatum; panel: Size; style: MetricStyle; onElementClick?: ElementClickListener; @@ -66,7 +66,7 @@ export const Metric: React.FunctionComponent<{ const interactionColor = changeColorLightness(datum.color, lightnessAmount, 0.8); const backgroundInteractionColor = changeColorLightness(style.background, lightnessAmount, 0.8); - const datumWithInteractionColor: MetricBase | MetricWProgress | MetricWTrend = { + const datumWithInteractionColor: MetricDatum = { ...datum, color: interactionColor, }; diff --git a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx index adbc7dc610..07378830e5 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx @@ -7,7 +7,7 @@ */ import classNames from 'classnames'; -import React from 'react'; +import React, { CSSProperties } from 'react'; import { Color } from '../../../../common/colors'; import { DEFAULT_FONT_FAMILY } from '../../../../common/default_theme_attributes'; @@ -17,7 +17,7 @@ import { isFiniteNumber, LayoutDirection, renderWithProps } from '../../../../ut import { Size } from '../../../../utils/dimensions'; import { wrapText } from '../../../../utils/text/wrap'; import { MetricStyle } from '../../../../utils/themes/theme'; -import { isMetricWProgress, MetricBase, MetricWProgress, MetricWTrend } from '../../specs'; +import { isMetricWNumber, isMetricWProgress, MetricDatum } from '../../specs'; type BreakPoint = 's' | 'm' | 'l'; @@ -50,7 +50,7 @@ type ElementVisibility = { }; function elementVisibility( - datum: MetricBase | MetricWProgress | MetricWTrend, + datum: MetricDatum, panel: Size, size: BreakPoint, ): ElementVisibility & { titleLines: string[]; subtitleLines: string[] } { @@ -136,10 +136,21 @@ function elementVisibility( }); } +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', + }; +} + /** @internal */ export const MetricText: React.FunctionComponent<{ id: string; - datum: MetricBase | MetricWProgress | MetricWTrend; + datum: MetricDatum; panel: Size; style: MetricStyle; onElementClick: () => void; @@ -158,10 +169,15 @@ export const MetricText: React.FunctionComponent<{ const visibility = elementVisibility(datum, panel, size); - const parts = splitNumericSuffixPrefix(datum.valueFormatter(value)); + const titleWidthMaxSize = size === 's' ? '100%' : '80%'; + const titlesWidth = `min(${titleWidthMaxSize}, calc(${titleWidthMaxSize} - ${datum.icon ? '24px' : '0px'}))`; - const titlesMaxWidthRatio = size === 's' ? '100%' : '80%'; - const titlesWidth = `min(${titlesMaxWidthRatio}, calc(${titlesMaxWidthRatio} - ${datum.icon ? '24px' : '0px'}))`; + const isNumericalMetric = isMetricWNumber(datum); + const textParts = isNumericalMetric + ? isFiniteNumber(value) + ? splitNumericSuffixPrefix(datum.valueFormatter(value)) + : [{ emphasis: 'normal', text: style.nonFiniteText }] + : [{ emphasis: 'normal', text: datum.value }]; return (
@@ -181,9 +197,11 @@ export const MetricText: React.FunctionComponent<{ fontSize: `${TITLE_FONT_SIZE[size]}px`, whiteSpace: 'pre-wrap', width: titlesWidth, + ...lineClamp(visibility.titleLines.length), }} + title={datum.title} > - {visibility.titleLines.join('\n')} + {datum.title} @@ -206,9 +224,11 @@ export const MetricText: React.FunctionComponent<{ fontSize: `${SUBTITLE_FONT_SIZE[size]}px`, width: titlesWidth, whiteSpace: 'pre-wrap', + ...lineClamp(visibility.subtitleLines.length), }} + title={datum.subtitle} > - {visibility.subtitleLines.join('\n')} + {datum.subtitle}

)}
@@ -221,36 +241,47 @@ export const MetricText: React.FunctionComponent<{ )}
-

- {isFiniteNumber(value) - ? parts.map(([type, text], i) => - type === 'numeric' ? ( - text - ) : ( - // eslint-disable-next-line react/no-array-index-key - - {text} - - ), - ) - : style.nonFiniteText} +

text).join('')} + > + {textParts.map(({ emphasis, text }, i) => { + return emphasis === 'small' ? ( + + {text} + + ) : ( + text + ); + })}

); }; -function splitNumericSuffixPrefix(text: string) { - const charts = text.split(''); - const parts = charts.reduce>((acc, curr) => { - const type = curr === '.' || curr === ',' || isFiniteNumber(Number.parseInt(curr)) ? 'numeric' : 'string'; - - if (acc.length > 0 && acc[acc.length - 1][0] === type) { - acc[acc.length - 1][1].push(curr); - } else { - acc.push([type, [curr]]); - } - return acc; - }, []); - return parts.map(([type, chars]) => [type, chars.join('')]); +function splitNumericSuffixPrefix(text: string): { emphasis: 'normal' | 'small'; text: string }[] { + return text + .split('') + .reduce<{ emphasis: 'normal' | 'small'; textParts: string[] }[]>((acc, curr) => { + const emphasis = curr === '.' || curr === ',' || isFiniteNumber(Number.parseInt(curr)) ? 'normal' : 'small'; + if (acc.length > 0 && acc[acc.length - 1].emphasis === emphasis) { + acc[acc.length - 1].textParts.push(curr); + } else { + acc.push({ emphasis, textParts: [curr] }); + } + return acc; + }, []) + .map(({ emphasis, textParts }) => ({ + emphasis, + text: textParts.join(''), + })); } diff --git a/packages/charts/src/chart_types/metric/specs/index.ts b/packages/charts/src/chart_types/metric/specs/index.ts index aa2701d1f7..647ae19417 100644 --- a/packages/charts/src/chart_types/metric/specs/index.ts +++ b/packages/charts/src/chart_types/metric/specs/index.ts @@ -18,8 +18,6 @@ import { LayoutDirection } from '../../../utils/common'; /** @alpha */ export type MetricBase = { - value: number; - valueFormatter: (d: number) => string; color: Color; title?: string; subtitle?: string; @@ -28,9 +26,20 @@ export type MetricBase = { }; /** @alpha */ -export type MetricWProgress = MetricBase & { +export type MetricWText = MetricBase & { + value: string; +}; + +/** @alpha */ +export type MetricWNumber = MetricBase & { + value: number; + valueFormatter: (d: number) => string; +}; + +/** @alpha */ +export type MetricWProgress = MetricWNumber & { domainMax: number; - progressBarDirection?: LayoutDirection; + progressBarDirection: LayoutDirection; }; /** @alpha */ @@ -43,18 +52,21 @@ export const MetricTrendShape = Object.freeze({ export type MetricTrendShape = $Values; /** @alpha */ -export type MetricWTrend = MetricBase & { +export type MetricWTrend = MetricWNumber & { trend: { x: number; y: number }[]; - trendShape?: MetricTrendShape; + trendShape: MetricTrendShape; trendA11yTitle?: string; trendA11yDescription?: string; }; +/** @alpha */ +export type MetricDatum = MetricWNumber | MetricWText | MetricWProgress | MetricWTrend; + /** @alpha */ export interface MetricSpec extends Spec { specType: typeof SpecType.Series; chartType: typeof ChartType.Metric; - data: (MetricBase | MetricWProgress | MetricWTrend | undefined)[][]; + data: (MetricDatum | undefined)[][]; } /** @alpha */ @@ -72,11 +84,22 @@ export const Metric = specComponentFactory()( export type MetricSpecProps = ComponentProps; /** @internal */ -export function isMetricWProgress(datum: MetricBase | MetricWProgress | MetricWTrend): datum is MetricWProgress { - return datum.hasOwnProperty('domainMax') && !datum.hasOwnProperty('trend'); +export function isMetricWNumber( + datum: MetricDatum, +): datum is MetricWNumber { + return typeof datum.value === 'number' && datum.hasOwnProperty('valueFormatter'); +} + +/** @internal */ +export function isMetricWProgress( + datum: MetricDatum, +): datum is MetricWProgress { + return isMetricWNumber(datum) && datum.hasOwnProperty('domainMax') && !datum.hasOwnProperty('trend'); } /** @internal */ -export function isMetricWTrend(datum: MetricBase | MetricWProgress | MetricWTrend): datum is MetricWTrend { - return datum.hasOwnProperty('trend') && !datum.hasOwnProperty('domainMax'); +export function isMetricWTrend( + datum: MetricDatum, +): datum is MetricWTrend { + return isMetricWNumber(datum) && datum.hasOwnProperty('trend') && !datum.hasOwnProperty('domainMax'); } diff --git a/packages/charts/src/chart_types/specs.ts b/packages/charts/src/chart_types/specs.ts index 11042a6e0d..68ef7dd098 100644 --- a/packages/charts/src/chart_types/specs.ts +++ b/packages/charts/src/chart_types/specs.ts @@ -36,7 +36,10 @@ export { MetricSpecProps, MetricSpec, MetricBase, + MetricWText, + MetricWNumber, MetricWProgress, MetricWTrend, MetricTrendShape, + MetricDatum, } from './metric/specs'; diff --git a/storybook/stories/metric/1_basic.story.tsx b/storybook/stories/metric/1_basic.story.tsx index 35825c718f..6fb0cbd1a8 100644 --- a/storybook/stories/metric/1_basic.story.tsx +++ b/storybook/stories/metric/1_basic.story.tsx @@ -11,7 +11,17 @@ import { action } from '@storybook/addon-actions'; import { select, boolean, text, color, number } from '@storybook/addon-knobs'; import React from 'react'; -import { Chart, isMetricElementEvent, Metric, MetricTrendShape, Settings } from '@elastic/charts'; +import { + Chart, + isMetricElementEvent, + Metric, + MetricTrendShape, + MetricWProgress, + MetricWTrend, + MetricWText, + MetricWNumber, + Settings, +} from '@elastic/charts'; import { KIBANA_METRICS } from '@elastic/charts/src/utils/data_samples/test_dataset_kibana'; import { useBaseTheme } from '../../use_base_theme'; @@ -44,7 +54,8 @@ export const Example = () => { let extra = text('extra', 'last 5m'); const progressMax = number('progress max', 100); - const value = number('value', 55.23); + const numberTextSwitch = boolean('is numeric metric', true); + const value = text('value', '55.23'); const valuePrefix = text('value prefix', ''); const valuePostfix = text('value postfix', ' %'); const metricColor = color('color', '#3c3c3c'); @@ -57,12 +68,17 @@ export const Example = () => { ({ width, height, color }: { width: number; height: number; color: string }) => ; const data = { - value, color: metricColor, title, subtitle, - valueFormatter: (d: number) => `${valuePrefix}${d}${valuePostfix}`, extra: , + ...(showIcon ? { icon: getIcon(iconType) } : {}), + }; + + const numericData: MetricWProgress | MetricWNumber | MetricWTrend = { + ...data, + value: Number.parseFloat(value), + valueFormatter: (d: number) => `${valuePrefix}${d}${valuePostfix}`, ...(progressOrTrend === 'bar' ? { domainMax: progressMax, progressBarDirection } : {}), ...(progressOrTrend === 'trend' ? { @@ -72,13 +88,17 @@ export const Example = () => { trendA11yDescription, } : {}), - ...(showIcon ? { icon: getIcon(iconType) } : {}), }; + const textualData: MetricWText = { + ...data, + value, + }; + const onEventClickAction = action('click'); const onEventOverAction = action('over'); const onEventOutAction = action('out'); - const configuredData = [[data]]; + const configuredData = [[numberTextSwitch ? numericData : textualData]]; return (
{ const defaultValueFormatter = (d: number) => `${d}`; const data: (MetricBase | MetricWProgress | MetricWTrend | undefined)[] = [ { - value: NaN, + color: '#3c3c3c', title: 'CPU Usage', subtitle: 'Overall percentage', + icon: getIcon('compute'), + value: NaN, + valueFormatter: defaultValueFormatter, trend: KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), + trendShape: 'area', trendA11yTitle: 'Last hour CPU percentage trend', trendA11yDescription: 'The trend shows the CPU Usage in percentage in the last hour. The trend shows a general flat behaviour with peaks every 10 minutes', - valueFormatter: defaultValueFormatter, - icon: getIcon('compute'), }, { - value: 33.57, color: '#FF7E62', title: 'Memory Usage', subtitle: 'Overall percentage', + value: 33.57, + valueFormatter: (d) => `${d} %`, trend: KIBANA_METRICS.metrics.kibana_memory[0].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), + trendShape: 'area', trendA11yTitle: 'Last hour Memory usage trend', trendA11yDescription: 'The trend shows the memory usage in the last hour. The trend shows a general flat behaviour across the entire time window', - valueFormatter: (d) => `${d} %`, }, { - value: 12.57, color: '#5e5e5e', title: 'Disk I/O', subtitle: 'Read', + icon: getIcon('sortUp'), + value: 12.57, valueFormatter: (d) => `${d} Mb/s`, ...(useProgressBar && { domainMax: 100, - progressBarDirection: progressBarDirection, + progressBarDirection, extra: ( max 100Mb/s ), }), - icon: getIcon('sortUp'), }, { - value: 41.12, + color: '#5e5e5e', title: 'Disk I/O', subtitle: 'Write', + icon: getIcon('sortDown'), + value: 41.12, valueFormatter: (d) => `${d} Mb/s`, ...(useProgressBar && { domainMax: 100, - progressBarDirection: progressBarDirection, + progressBarDirection, extra: ( max 100Mb/s ), }), - icon: getIcon('sortDown'), }, { - value: 24.85, color: '#6DCCB1', title: '21d7f8b7-92ea-41a0-8c03-0db0ec7e11b9', subtitle: 'Cluster CPU Usage', - trend: KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), + value: 24.85, valueFormatter: (d) => `${d}%`, + trend: KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), + trendShape: 'area', }, { - value: 3.57, color: '#FFBDAF', title: 'Inbound Traffic', subtitle: 'Network eth0', - valueFormatter: (d) => `${d}KBps`, extra: ( last 5m ), icon: getIcon('sortUp'), + value: 3.57, + valueFormatter: (d) => `${d}KBps`, }, undefined, { - value: 323.57, color: '#F1D86F', title: 'Cloud Revenue', subtitle: 'Quarterly', - trend: KIBANA_METRICS.metrics.kibana_os_load[2].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), - trendA11yTitle: 'Last quarter, daily Cloud Revenue trend', - trendA11yDescription: - 'The trend shows the daily Cloud revenue in the last quarter, showing peaks during weekends.', extra: ( This Year 10M ), + value: 323.57, valueFormatter: (d) => `$ ${d}k`, + trend: KIBANA_METRICS.metrics.kibana_os_load[2].data.slice(0, maxDataPoints).map(([x, y]) => ({ x, y })), + trendShape: 'area', + trendA11yTitle: 'Last quarter, daily Cloud Revenue trend', + trendA11yDescription: + 'The trend shows the daily Cloud revenue in the last quarter, showing peaks during weekends.', }, ];