Skip to content

Commit

Permalink
feat(metric): allow alpha colors and improve contrast logic (elastic#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nickofthyme authored Oct 10, 2023
1 parent ec95d50 commit dd5732e
Show file tree
Hide file tree
Showing 41 changed files with 223 additions and 85 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 18 additions & 3 deletions e2e/tests/metric_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { test } from '@playwright/test';

import { eachTheme, pwEach } from '../helpers';
import { common } from '../page_objects';

test.describe('Metric', () => {
Expand All @@ -21,7 +22,7 @@ test.describe('Metric', () => {
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:eui-light&knob-use progress bar=&knob-progress bar direction=horizontal&knob-max trend data points=30&knob-layout=grid',
);
});
test('should render vertical progress bar in dark mode', async ({ page }) => {
test('should render vertical progress bar in dark mode', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:eui-dark&knob-layout=grid&knob-max trend data points=30&knob-progress bar direction=vertical&knob-use progress bar=true',
);
Expand All @@ -31,19 +32,33 @@ test.describe('Metric', () => {
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:eui-dark&knob-layout=grid&knob-max trend data points=30&knob-progress bar direction=horizontal&knob-use progress bar=true',
);
});
test('should render no progress bar in dark mode', async ({ page }) => {
test('should render no progress bar in dark mode', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:eui-dark&knob-layout=grid&knob-max trend data points=30&knob-progress bar direction=horizontal&knob-use progress bar=',
);
});
test('text value with trend', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-light&knob-EUI icon glyph name=warning&knob-color=rgba(166, 219, 208, 0.47)&knob-extra=1310 (-74% week before)&knob-is numeric metric=false&knob-progress bar direction=vertical&knob-progress max=100&knob-progress or trend=trend&knob-subtitle=&knob-title=Most used in&knob-trend a11y description=The trend shows a peak of CPU usage in the last 5 minutes&knob-trend a11y title=The Cluster CPU Usage trend&knob-trend data points=30&knob-trend shape=area&knob-value=United States&knob-value postfix=&knob-value prefix=&knob-show icon=',
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-light&knob-EUI icon glyph name=warning&knob-color=rgba(166, 219, 208, 1)&knob-extra=1310 (-74% week before)&knob-is numeric metric=false&knob-progress bar direction=vertical&knob-progress max=100&knob-progress or trend=trend&knob-subtitle=&knob-title=Most used in&knob-trend a11y description=The trend shows a peak of CPU usage in the last 5 minutes&knob-trend a11y title=The Cluster CPU Usage trend&knob-trend data points=30&knob-trend shape=area&knob-value=United States&knob-value postfix=&knob-value prefix=&knob-show icon=',
);
});
test('value icon and value color', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-light&knob-title=Network out&knob-subtitle=host: 1dc4e&knob-progress or trend=trend&knob-progress bar direction=vertical&knob-trend data points=30&knob-trend shape=area&knob-trend a11y title=The Cluster CPU Usage trend&knob-trend a11y description=The trend shows a peak of CPU usage in the last 5 minutes&knob-extra=last <b>5m</b>&knob-progress max=100&knob-is numeric metric=true&knob-value=55.23&knob-value prefix=&knob-value postfix=GB&knob-color=rgba(255, 255, 255, 1)&knob-use value color=true&knob-value color=rgba(189, 0, 0, 1)&knob-show icon=true&knob-EUI icon glyph name=warning&knob-show value icon=true&knob-EUI value icon glyph name=sortUp',
);
});

pwEach.describe(['trend', 'bar', 'none'])(
(v) => `Metric - ${v} type`,
(type) => {
eachTheme.test(
async ({ page, urlParam }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
`http://localhost:9001/?path=/story/metric-alpha--basic&${urlParam}&knob-EUI icon glyph name=warning&knob-EUI value icon glyph name=sortUp&knob-color=rgba(157, 66, 66, 0.44)&knob-extra=last <b>5m</b>&knob-is numeric metric=true&knob-progress bar direction=vertical&knob-progress max=100&knob-progress or trend=${type}&knob-subtitle=Cluster CPU usage&knob-title=21d7f8b7-92ea-41a0-8c03-0db0ec7e11b9&knob-trend a11y description=The trend shows a peak of CPU usage in the last 5 minutes&knob-trend a11y title=The Cluster CPU Usage trend&knob-trend data points=30&knob-trend shape=area&knob-value=55.23&knob-value color=#3c3c3c&knob-value prefix=&knob-value postfix= %&knob-use value color=&knob-show icon=&knob-show value icon=`,
);
},
(t) => `should render metric with transparent bg color - ${t} theme`,
);
},
);
});
2 changes: 0 additions & 2 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1840,8 +1840,6 @@ export type MetricSpecProps = ComponentProps<typeof Metric>;

// @public (undocumented)
export interface MetricStyle {
// (undocumented)
background: Color;
// (undocumented)
barBackground: Color;
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function shapeViewModel<D extends BaseDatum = Datum>(
const cellHeightInner = cellHeight - gridStrokeWidth;

if (colorToRgba(background.color)[3] < 1) {
// TODO: centralize this check and bg color fallback across all chart types
Logger.expected(
'Text contrast requires a opaque background color, using fallbackColor',
'an opaque color',
Expand Down Expand Up @@ -200,7 +201,7 @@ export function shapeViewModel<D extends BaseDatum = Datum>(
visible: !isValueInRanges(d.value, bandsToHide),
formatted: formattedValue,
fontSize,
textColor: fillTextColor(background.fallbackColor, cellBackgroundColor, background.color),
textColor: fillTextColor(background.fallbackColor, cellBackgroundColor, background.color).color.keyword,
});
return acc;
}, new Map());
Expand Down
28 changes: 18 additions & 10 deletions packages/charts/src/chart_types/metric/renderer/dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { Metric as MetricComponent } from './metric';
import { highContrastColor } from '../../../../common/color_calcs';
import { ColorContrastOptions, highContrastColor } from '../../../../common/color_calcs';
import { colorToRgba } from '../../../../common/color_library_wrappers';
import { Colors } from '../../../../common/colors';
import { getResolvedBackgroundColor } from '../../../../common/fill_text_color';
import { BasicListener, ElementClickListener, ElementOverListener, settingsBuildProps } from '../../../../specs';
import { onChartRendered } from '../../../../state/actions/chart';
import { GlobalChartState } from '../../../../state/chart_state';
Expand All @@ -30,7 +30,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';
import { MetricStyle } from '../../../../utils/themes/theme';
import { BackgroundStyle, MetricStyle } from '../../../../utils/themes/theme';
import { MetricSpec } from '../../specs';
import { chartSize } from '../../state/selectors/chart_size';
import { getMetricSpecs } from '../../state/selectors/data';
Expand All @@ -47,6 +47,7 @@ interface StateProps {
specs: MetricSpec[];
a11y: A11ySettings;
style: MetricStyle;
background: BackgroundStyle;
locale: string;
onElementClick?: ElementClickListener;
onElementOut?: BasicListener;
Expand Down Expand Up @@ -76,6 +77,7 @@ class Component extends React.Component<StateProps & DispatchProps> {
a11y,
specs: [spec], // ignoring other specs
style,
background,
onElementClick,
onElementOut,
onElementOver,
Expand All @@ -93,11 +95,12 @@ class Component extends React.Component<StateProps & DispatchProps> {
}, 0);

const panel = { width: width / maxColumns, height: height / totalRows };

const emptyForegroundColor =
highContrastColor(colorToRgba(style.background)) === Colors.White.rgba
? style.text.lightColor
: style.text.darkColor;
const backgroundColor = getResolvedBackgroundColor(background.fallbackColor, background.color);
const contrastOptions: ColorContrastOptions = {
lightColor: colorToRgba(style.text.lightColor),
darkColor: colorToRgba(style.text.darkColor),
};
const { color: emptyForegroundColor } = highContrastColor(colorToRgba(backgroundColor), undefined, contrastOptions);

return (
// eslint-disable-next-line jsx-a11y/no-redundant-roles
Expand All @@ -123,7 +126,7 @@ class Component extends React.Component<StateProps & DispatchProps> {
return !datum ? (
<li key={`${columnIndex}-${rowIndex}`} role="presentation">
<div className={emptyMetricClassName} style={{ borderColor: style.border }}>
<div className="echMetricEmpty" style={{ borderColor: emptyForegroundColor }}></div>
<div className="echMetricEmpty" style={{ borderColor: emptyForegroundColor.keyword }}></div>
</div>
</li>
) : (
Expand All @@ -138,6 +141,8 @@ class Component extends React.Component<StateProps & DispatchProps> {
columnIndex={columnIndex}
panel={panel}
style={style}
backgroundColor={backgroundColor}
contrastOptions={contrastOptions}
onElementClick={onElementClick}
onElementOut={onElementOut}
onElementOver={onElementOver}
Expand Down Expand Up @@ -185,6 +190,7 @@ const DEFAULT_PROPS: StateProps = {
},
a11y: DEFAULT_A11Y_SETTINGS,
style: LIGHT_THEME.metric,
background: LIGHT_THEME.background,
locale: settingsBuildProps.defaults.locale,
};

Expand All @@ -193,6 +199,7 @@ const mapStateToProps = (state: GlobalChartState): StateProps => {
return DEFAULT_PROPS;
}
const { onElementClick, onElementOut, onElementOver, locale } = getSettingsSpecSelector(state);
const { metric: style, background } = getChartThemeSelector(state);
return {
initialized: true,
chartId: state.chartId,
Expand All @@ -203,7 +210,8 @@ const mapStateToProps = (state: GlobalChartState): StateProps => {
onElementClick,
onElementOver,
onElementOut,
style: getChartThemeSelector(state).metric,
background,
style,
locale,
};
};
Expand Down
52 changes: 35 additions & 17 deletions packages/charts/src/chart_types/metric/renderer/dom/metric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import classNames from 'classnames';
import React, { CSSProperties, useState } from 'react';

import { ProgressBar } from './progress';
import { SparkLine } from './sparkline';
import { SparkLine, getSparkLineColor } from './sparkline';
import { MetricText } from './text';
import { highContrastColor } from '../../../../common/color_calcs';
import { changeColorLightness, colorToRgba } from '../../../../common/color_library_wrappers';
import { Colors } from '../../../../common/colors';
import { ColorContrastOptions } from '../../../../common/color_calcs';
import { changeColorLightness } from '../../../../common/color_library_wrappers';
import { Color } from '../../../../common/colors';
import { DEFAULT_CSS_CURSOR } from '../../../../common/constants';
import { fillTextColor } from '../../../../common/fill_text_color';
import {
BasicListener,
ElementClickListener,
Expand All @@ -39,6 +40,8 @@ export const Metric: React.FunctionComponent<{
datum: MetricDatum;
panel: Size;
style: MetricStyle;
backgroundColor: Color;
contrastOptions: ColorContrastOptions;
locale: string;
onElementClick?: ElementClickListener;
onElementOver?: ElementOverListener;
Expand All @@ -53,6 +56,8 @@ export const Metric: React.FunctionComponent<{
datum,
panel,
style,
backgroundColor,
contrastOptions,
locale,
onElementClick,
onElementOver,
Expand All @@ -75,34 +80,47 @@ export const Metric: React.FunctionComponent<{

const lightnessAmount = mouseState === 'leave' ? 0 : mouseState === 'enter' ? 0.05 : 0.1;
const interactionColor = changeColorLightness(datum.color, lightnessAmount, 0.8);
const backgroundInteractionColor = changeColorLightness(style.background, lightnessAmount, 0.8);

const datumWithInteractionColor: MetricDatum = {
...datum,
color: interactionColor,
};
const updatedStyle: MetricStyle = {
...style,
background: backgroundInteractionColor,
};

const event: MetricElementEvent = { type: 'metricElementEvent', rowIndex, columnIndex };

const containerStyle: CSSProperties = {
backgroundColor:
!isMetricWTrend(datumWithInteractionColor) && !isMetricWProgress(datumWithInteractionColor)
? datumWithInteractionColor.color
: updatedStyle.background,
: undefined,
cursor: onElementClick ? 'pointer' : DEFAULT_CSS_CURSOR,
borderColor: style.border,
};

const bgColor = isMetricWTrend(datum) || !isMetricWProgress(datum) ? datum.color : style.background;
const highContrastTextColor = fillTextColor(
backgroundColor,
isMetricWProgress(datum) ? backgroundColor : datum.color,
undefined,
contrastOptions,
);
let finalTextColor = highContrastTextColor.color;

if (isMetricWTrend(datum)) {
const { ratio, color, shade } = fillTextColor(
backgroundColor,
getSparkLineColor(datum.color),
undefined,
contrastOptions,
);

const highContrastTextColor =
highContrastColor(colorToRgba(bgColor)) === Colors.White.rgba ? style.text.lightColor : style.text.darkColor;
// TODO verify this check is applied correctly
if (shade !== highContrastTextColor.shade && ratio > highContrastTextColor.ratio) {
finalTextColor = color;
}
}

const onElementClickHandler = () => onElementClick && onElementClick([event]);

return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions,jsx-a11y/click-events-have-key-events
<div
Expand Down Expand Up @@ -142,16 +160,16 @@ export const Metric: React.FunctionComponent<{
id={metricHTMLId}
datum={datumWithInteractionColor}
panel={panel}
style={updatedStyle}
style={style}
onElementClick={onElementClick ? onElementClickHandler : undefined}
highContrastTextColor={highContrastTextColor}
highContrastTextColor={finalTextColor.keyword}
locale={locale}
/>
{isMetricWTrend(datumWithInteractionColor) && <SparkLine id={metricHTMLId} datum={datumWithInteractionColor} />}
{isMetricWProgress(datumWithInteractionColor) && (
<ProgressBar datum={datumWithInteractionColor} barBackground={updatedStyle.barBackground} />
<ProgressBar datum={datumWithInteractionColor} barBackground={style.barBackground} />
)}
<div className="echMetric--outline" style={{ color: highContrastTextColor }}></div>
<div className="echMetric--outline" style={{ color: finalTextColor.keyword }}></div>
</div>
);
};
26 changes: 22 additions & 4 deletions packages/charts/src/chart_types/metric/renderer/dom/sparkline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import { isFiniteNumber } from '../../../../utils/common';
import { CurveType } from '../../../../utils/curves';
import { MetricTrendShape, MetricWTrend } from '../../specs';

/** @internal */
export const getSparkLineColor = (color: MetricWTrend['color']) => {
const [h, s, l, a] = colorToHsl(color);
return hslToColor(h, s, l >= 0.8 ? l - 0.1 : l + 0.1, a);
};

/** @internal */
export const SparkLine: FunctionComponent<{
id: string;
Expand All @@ -36,8 +42,6 @@ export const SparkLine: FunctionComponent<{
trendShape === MetricTrendShape.Bars ? CurveType.CURVE_STEP_AFTER : CurveType.LINEAR,
);

const [h, s, l] = colorToHsl(color);
const pathColor = hslToColor(h, s, l >= 0.8 ? l - 0.1 : l + 0.1);
const titleId = `${id}-trend-title`;
const descriptionId = `${id}-trend-description`;
return (
Expand All @@ -51,17 +55,31 @@ export const SparkLine: FunctionComponent<{
role="img"
aria-labelledby={`${titleId} ${descriptionId}`}
>
<defs>
<mask id="sparkline-mask">
<rect x={0} y={0} width={1} height={1} fill="white" mask="url(#sparkline-mask)" />
<path
d={path.area(trend)}
transform="translate(0, 0.5),scale(1,0.5)"
fill="black"
stroke="none"
strokeWidth={0}
/>
</mask>
</defs>

<title id={titleId} className="echScreenReaderOnly">
{trendA11yTitle}
</title>
<text id={descriptionId} className="echScreenReaderOnly" fontSize={0}>
{trendA11yDescription}
</text>
<rect x={0} y={0} width={1} height={1} fill={color} />

<rect x={0} y={0} width={1} height={1} fill={color} mask="url(#sparkline-mask)" />
<path
d={path.area(trend)}
transform="translate(0, 0.5),scale(1,0.5)"
fill={pathColor}
fill={getSparkLineColor(color)}
stroke="none"
strokeWidth={0}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,6 @@ describe('Test fillTextColor function', () => {
const fillColor = 'rgba(55, 126, 184, 0.7)';
const containerBackgroundColor = 'white';
const expectedAdjustedTextColor = 'rgba(0, 0, 0, 1)'; // with WCAG 2 is black
expect(fillTextColor(fillColor, containerBackgroundColor)).toEqual(expectedAdjustedTextColor);
expect(fillTextColor(fillColor, containerBackgroundColor).color.keyword).toEqual(expectedAdjustedTextColor);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function linkTextLayout(
}
const textColor =
linkLabel.textColor === ColorVariant.Adaptive
? fillTextColor(fallbackBGColor, null, backgroundColor)
? fillTextColor(fallbackBGColor, null, backgroundColor).color.keyword
: linkLabel.textColor;
const labelFontSpec: Font = { ...linkLabel, textColor };
const valueFontSpec: Font = { ...linkLabel, ...linkLabel.valueFont, textColor };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function makeQuadViewModel(
const textColor = textNegligible
? Colors.Transparent.keyword
: fillLabel.textColor === ColorVariant.Adaptive
? fillTextColor(fallbackBGColor, fillColor, backgroundColor)
? fillTextColor(fallbackBGColor, fillColor, backgroundColor).color.keyword
: fillLabel.textColor;

return { index, innerIndex, smAccessorValue, strokeWidth, strokeStyle, fillColor, textColor, ...node };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ function getTextColors(
shadowColor: fillDefinition.borderColor || Colors.Transparent.keyword,
};
}
const fillColor = fillTextColor(fallbackBGColor, geometryColor, backgroundColor);
const shadowColor = fillTextColor(fallbackBGColor, fillColor, backgroundColor);
const fillColor = fillTextColor(fallbackBGColor, geometryColor, backgroundColor).color.keyword;
const shadowColor = fillTextColor(fallbackBGColor, fillColor, backgroundColor).color.keyword;

return {
fillColor,
Expand Down
4 changes: 2 additions & 2 deletions packages/charts/src/common/color_calcs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ describe('Color calcs', () => {
const background: RgbaTuple = [120, 116, 178, 0.7];
const blendedBackground: RgbaTuple = [161, 158, 201, 1];
expect(combineColors(background, Colors.White.rgba)).toEqual(blendedBackground);
expect(highContrastColor(blendedBackground, 'WCAG2')).toEqual(Colors.Black.rgba);
expect(highContrastColor(blendedBackground, 'WCAG3')).toEqual(Colors.White.rgba);
expect(highContrastColor(blendedBackground, 'WCAG2').color.rgba).toEqual(Colors.Black.rgba);
expect(highContrastColor(blendedBackground, 'WCAG3').color.rgba).toEqual(Colors.White.rgba);
});
});
describe('test the combineColors function', () => {
Expand Down
Loading

0 comments on commit dd5732e

Please sign in to comment.