Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(metric): add text value #1817

Merged
merged 13 commits into from
Sep 15, 2022
Merged
26 changes: 19 additions & 7 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1718,8 +1718,6 @@ export const Metric: FC<SFProps<MetricSpec, "chartType" | "specType", "data", ne

nickofthyme marked this conversation as resolved.
Show resolved Hide resolved
// @alpha (undocumented)
export type MetricBase = {
value: number;
valueFormatter: (d: number) => string;
color: Color;
title?: string;
subtitle?: string;
Expand All @@ -1731,6 +1729,9 @@ export type MetricBase = {
}>;
};

// @alpha (undocumented)
export type MetricDatum = MetricWNumber | MetricWText | MetricWProgress | MetricWTrend;

// @public
export type MetricElementEvent = {
type: 'metricElementEvent';
Expand All @@ -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;
}
Expand Down Expand Up @@ -1776,18 +1777,29 @@ export const MetricTrendShape: Readonly<{
export type MetricTrendShape = $Values<typeof MetricTrendShape>;

// @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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
font-weight: 600;
text-align: right;
white-space: nowrap;
overflow: hidden;
}

&__part {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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,
};
Expand Down
95 changes: 62 additions & 33 deletions packages/charts/src/chart_types/metric/renderer/dom/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -50,7 +50,7 @@ type ElementVisibility = {
};

function elementVisibility(
datum: MetricBase | MetricWProgress | MetricWTrend,
datum: MetricDatum,
panel: Size,
size: BreakPoint,
): ElementVisibility & { titleLines: string[]; subtitleLines: string[] } {
Expand Down Expand Up @@ -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;
Expand All @@ -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))
: [['normal', style.nonFiniteText]]
: [['normal', datum.value]];

return (
<div className={containerClassName} style={{ color: highContrastTextColor }}>
Expand All @@ -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}
</span>
</button>
</h2>
Expand All @@ -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}
</p>
)}
</div>
Expand All @@ -221,36 +241,45 @@ export const MetricText: React.FunctionComponent<{
)}
</div>
<div>
<p className="echMetricText__value" style={{ fontSize: `${VALUE_FONT_SIZE[size]}px` }}>
{isFiniteNumber(value)
? parts.map(([type, text], i) =>
type === 'numeric' ? (
text
) : (
// eslint-disable-next-line react/no-array-index-key
<span key={i} className="echMetricText__part" style={{ fontSize: `${VALUE_PART_FONT_SIZE[size]}px` }}>
{text}
</span>
),
)
: style.nonFiniteText}
<p
className="echMetricText__value"
style={{
fontSize: `${VALUE_FONT_SIZE[size]}px`,
textOverflow: isNumericalMetric ? undefined : 'ellipsis',
}}
title={textParts.map(([, text]) => text).join('')}
>
{textParts.map(([emphasis, text], i) => {
return emphasis === 'small' ? (
<span
key={`${text}${i}`}
className="echMetricText__part"
style={{ fontSize: `${VALUE_PART_FONT_SIZE[size]}px` }}
>
{text}
</span>
) : (
text
);
})}
</p>
</div>
</div>
);
};

function splitNumericSuffixPrefix(text: string) {
function splitNumericSuffixPrefix(text: string): ['normal' | 'small', string][] {
markov00 marked this conversation as resolved.
Show resolved Hide resolved
const charts = text.split('');
const parts = charts.reduce<Array<[string, string[]]>>((acc, curr) => {
const type = curr === '.' || curr === ',' || isFiniteNumber(Number.parseInt(curr)) ? 'numeric' : 'string';
return charts
.reduce<Array<['normal' | 'small', string[]]>>((acc, curr) => {
const type = curr === '.' || curr === ',' || isFiniteNumber(Number.parseInt(curr)) ? 'normal' : 'small';

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('')]);
if (acc.length > 0 && acc[acc.length - 1][0] === type) {
acc[acc.length - 1][1].push(curr);
} else {
acc.push([type, [curr]]);
}
return acc;
}, [])
.map(([type, chars]) => [type, chars.join('')]);
}
45 changes: 34 additions & 11 deletions packages/charts/src/chart_types/metric/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 */
Expand All @@ -43,18 +52,21 @@ export const MetricTrendShape = Object.freeze({
export type MetricTrendShape = $Values<typeof MetricTrendShape>;

/** @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 */
Expand All @@ -72,11 +84,22 @@ export const Metric = specComponentFactory<MetricSpec>()(
export type MetricSpecProps = ComponentProps<typeof Metric>;

/** @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');
}
3 changes: 3 additions & 0 deletions packages/charts/src/chart_types/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export {
MetricSpecProps,
MetricSpec,
MetricBase,
MetricWText,
MetricWNumber,
MetricWProgress,
MetricWTrend,
MetricTrendShape,
MetricDatum,
} from './metric/specs';
Loading