Skip to content

Commit

Permalink
feat(metric): make value fit logic uniform across panels (#2484)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickofthyme authored Jul 15, 2024
1 parent 82b84b9 commit 991347d
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 170 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions e2e/tests/metric_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ test.describe('Metric', () => {
);
});

test('should render metric value fit font size', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/metric-alpha--array-of-values&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Blue groupId_Annotations=primary&knob-Dataset_Legend=shortCopyDataset&knob-Domain axis_Annotations=y&knob-EUI icon glyph name=warning&knob-EUI value icon glyph name=sortUp&knob-Enable debug state=true&knob-FadeOnFocusingOthers_Animations=true&knob-Hide color picker_Legend=true&knob-Legend Value_Legend=median,min,max&knob-Legend position_Legend=right&knob-Number formatting precision_Legend=2&knob-Outside dimension_Annotations=4&knob-Popover position_Legend=leftCenter&knob-Red groupId_Annotations=primary&knob-Render outside chart_Annotations=true&knob-Scale type=linear&knob-Series type=area&knob-SeriesType=bar&knob-Tick size=10&knob-annotation count_Styles=6&knob-annotation opacity_Styles=0.5&knob-annotation zIndex_Styles=0&knob-attach click handler=true&knob-blending background=rgba(255,255,255,1)&knob-chartRotation=180&knob-empty background=transparent&knob-max trend data points=30&knob-number of columns=3&knob-value font mode=fit&knob-value font size (px)=40&knob-show grid border=&knob-debug randomized data=',
);
});

pwEach.describe(['trend', 'bar', 'none'])(
(v) => `Metric - ${v} type`,
(type) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ class Component extends React.Component<Props> {
textLightColor: 'white',
textDarkColor: 'black',
nonFiniteText: 'N/A',
valueFontSize: 'default', // bullet does not support fit mode
})}
locale={locale}
backgroundColor={backgroundColor}
contrastOptions={contrastOptions}
panel={{ width: size.width / stats.columns, height: size.height / stats.rows }}
fittedValueFontSize={NaN}
/>
);
}}
Expand Down
261 changes: 141 additions & 120 deletions packages/charts/src/chart_types/metric/renderer/dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
/* eslint-disable react/no-array-index-key */

import classNames from 'classnames';
import React from 'react';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { Metric as MetricComponent } from './metric';
import { getFitValueFontSize, getMetricTextPartDimensions } from './text';
import { ColorContrastOptions, combineColors, highContrastColor } from '../../../../common/color_calcs';
import { colorToRgba, RGBATupleToString } from '../../../../common/color_library_wrappers';
import { Color } from '../../../../common/colors';
import { BasicListener, ElementClickListener, ElementOverListener, settingsBuildProps } from '../../../../specs';
import { onChartRendered } from '../../../../state/actions/chart';
import { onChartRendered as onChartRenderedAction } from '../../../../state/actions/chart';
import { GlobalChartState } from '../../../../state/chart_state';
import {
A11ySettings,
Expand Down Expand Up @@ -56,134 +57,154 @@ interface StateProps {
}

interface DispatchProps {
onChartRendered: typeof onChartRendered;
onChartRendered: typeof onChartRenderedAction;
}

class Component extends React.Component<StateProps & DispatchProps> {
static displayName = 'Metric';
componentDidMount() {
this.props.onChartRendered();
function Component({
chartId,
hasTitles,
initialized,
size: { width, height },
a11y,
specs: [spec],
style,
backgroundColor,
onElementClick,
onElementOut,
onElementOver,
locale,
onChartRendered,
}: StateProps & DispatchProps) {
useEffect(() => {
onChartRendered();
});

if (!initialized || !spec || width === 0 || height === 0) {
return null;
}

componentDidUpdate() {
this.props.onChartRendered();
}
const { data } = spec;

render() {
const {
chartId,
hasTitles,
initialized,
size: { width, height },
a11y,
specs: [spec], // ignoring other specs
style,
backgroundColor,
onElementClick,
onElementOut,
onElementOver,
locale,
} = this.props;
if (!initialized || !spec || width === 0 || height === 0) {
return null;
}

const { data } = spec;

const totalRows = data.length;
const maxColumns = data.reduce((acc, row) => {
return Math.max(acc, row.length);
}, 0);

const panel = { width: width / maxColumns, height: height / totalRows };
const contrastOptions: ColorContrastOptions = {
lightColor: colorToRgba(style.textLightColor),
darkColor: colorToRgba(style.textDarkColor),
};

const emptyBackgroundRGBA = combineColors(colorToRgba(style.emptyBackground), colorToRgba(backgroundColor));
const emptyBackground = RGBATupleToString(emptyBackgroundRGBA);
const { color: emptyForegroundColor } = highContrastColor(emptyBackgroundRGBA, undefined, contrastOptions);

return (
// eslint-disable-next-line jsx-a11y/no-redundant-roles
<ul
role="list"
className="echMetricContainer"
aria-labelledby={a11y.labelId}
aria-describedby={a11y.descriptionId}
style={{
gridTemplateColumns: `repeat(${maxColumns}, minmax(0, 1fr)`,
gridTemplateRows: `repeat(${totalRows}, minmax(${style.minHeight}px, 1fr)`,
}}
>
{data.flatMap((columns, rowIndex) => {
return [
...columns.map((datum, columnIndex) => {
// fill undefined with empty panels
const emptyMetricClassName = classNames('echMetric', {
'echMetric--rightBorder': columnIndex < maxColumns - 1,
'echMetric--bottomBorder': rowIndex < totalRows - 1,
'echMetric--topBorder': hasTitles && rowIndex === 0,
});
return !datum ? (
<li key={`${columnIndex}-${rowIndex}`} role="presentation">
<div
className={emptyMetricClassName}
style={{ borderColor: style.border, backgroundColor: emptyBackground }}
>
<div className="echMetricEmpty" style={{ borderColor: emptyForegroundColor.keyword }}></div>
</div>
</li>
) : (
<li key={`${columnIndex}-${rowIndex}`}>
<MetricComponent
chartId={chartId}
hasTitles={hasTitles}
datum={datum}
totalRows={totalRows}
totalColumns={maxColumns}
rowIndex={rowIndex}
columnIndex={columnIndex}
panel={panel}
style={style}
backgroundColor={backgroundColor}
contrastOptions={contrastOptions}
onElementClick={onElementClick}
onElementOut={onElementOut}
onElementOver={onElementOver}
locale={locale}
/>
</li>
);
}),
// fill the grid row with empty panels
...Array.from({ length: maxColumns - columns.length }, (_, zeroBasedColumnIndex) => {
const columnIndex = zeroBasedColumnIndex + columns.length;
const emptyMetricClassName = classNames('echMetric', {
'echMetric--bottomBorder': rowIndex < totalRows - 1,
'echMetric--topBorder': hasTitles && rowIndex === 0,
});
return (
<li key={`missing-${columnIndex}-${rowIndex}`} role="presentation">
<div
className={emptyMetricClassName}
style={{ borderColor: style.border, backgroundColor: emptyBackground }}
></div>
</li>
);
}),
];
})}
</ul>
);
}
const totalRows = data.length;
const maxColumns = data.reduce((acc, row) => {
return Math.max(acc, row.length);
}, 0);

const panel = { width: width / maxColumns, height: height / totalRows };
const contrastOptions: ColorContrastOptions = {
lightColor: colorToRgba(style.textLightColor),
darkColor: colorToRgba(style.textDarkColor),
};

const emptyBackgroundRGBA = combineColors(colorToRgba(style.emptyBackground), colorToRgba(backgroundColor));
const emptyBackground = RGBATupleToString(emptyBackgroundRGBA);
const { color: emptyForegroundColor } = highContrastColor(emptyBackgroundRGBA, undefined, contrastOptions);

const fittedValueFontSize =
style.valueFontSize !== 'fit'
? NaN
: data
.flat()
.filter((d) => d !== undefined)
.reduce((acc, datum) => {
const { sizes, progressBarWidth, visibility, textParts } = getMetricTextPartDimensions(
datum,
panel,
style,
locale,
);
const fontSize = getFitValueFontSize(
sizes.valueFontSize,
panel.width - progressBarWidth,
visibility.gapHeight,
textParts,
style.minValueFontSize,
datum.valueIcon !== undefined,
);
return Math.min(acc, fontSize);
}, Number.MAX_SAFE_INTEGER);

return (
// eslint-disable-next-line jsx-a11y/no-redundant-roles
<ul
role="list"
className="echMetricContainer"
aria-labelledby={a11y.labelId}
aria-describedby={a11y.descriptionId}
style={{
gridTemplateColumns: `repeat(${maxColumns}, minmax(0, 1fr)`,
gridTemplateRows: `repeat(${totalRows}, minmax(${style.minHeight}px, 1fr)`,
}}
>
{data.flatMap((columns, rowIndex) => {
return [
...columns.map((datum, columnIndex) => {
// fill undefined with empty panels
const emptyMetricClassName = classNames('echMetric', {
'echMetric--rightBorder': columnIndex < maxColumns - 1,
'echMetric--bottomBorder': rowIndex < totalRows - 1,
'echMetric--topBorder': hasTitles && rowIndex === 0,
});
return !datum ? (
<li key={`${columnIndex}-${rowIndex}`} role="presentation">
<div
className={emptyMetricClassName}
style={{ borderColor: style.border, backgroundColor: emptyBackground }}
>
<div className="echMetricEmpty" style={{ borderColor: emptyForegroundColor.keyword }}></div>
</div>
</li>
) : (
<li key={`${columnIndex}-${rowIndex}`}>
<MetricComponent
chartId={chartId}
hasTitles={hasTitles}
datum={datum}
totalRows={totalRows}
totalColumns={maxColumns}
rowIndex={rowIndex}
columnIndex={columnIndex}
panel={panel}
style={style}
backgroundColor={backgroundColor}
contrastOptions={contrastOptions}
onElementClick={onElementClick}
onElementOut={onElementOut}
onElementOver={onElementOver}
locale={locale}
fittedValueFontSize={fittedValueFontSize}
/>
</li>
);
}),
// fill the grid row with empty panels
...Array.from({ length: maxColumns - columns.length }, (_, zeroBasedColumnIndex) => {
const columnIndex = zeroBasedColumnIndex + columns.length;
const emptyMetricClassName = classNames('echMetric', {
'echMetric--bottomBorder': rowIndex < totalRows - 1,
'echMetric--topBorder': hasTitles && rowIndex === 0,
});
return (
<li key={`missing-${columnIndex}-${rowIndex}`} role="presentation">
<div
className={emptyMetricClassName}
style={{ borderColor: style.border, backgroundColor: emptyBackground }}
></div>
</li>
);
}),
];
})}
</ul>
);
}

Component.displayName = 'Metric';

const mapDispatchToProps = (dispatch: Dispatch): DispatchProps =>
bindActionCreators(
{
onChartRendered,
onChartRendered: onChartRenderedAction,
},
dispatch,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Metric: React.FunctionComponent<{
backgroundColor: Color;
contrastOptions: ColorContrastOptions;
locale: string;
fittedValueFontSize: number;
onElementClick?: ElementClickListener;
onElementOver?: ElementOverListener;
onElementOut?: BasicListener;
Expand All @@ -62,6 +63,7 @@ export const Metric: React.FunctionComponent<{
onElementClick,
onElementOver,
onElementOut,
fittedValueFontSize,
}) => {
const progressBarSize = 'small'; // currently we provide only the small progress bar;
const [mouseState, setMouseState] = useState<'leave' | 'enter' | 'down'>('leave');
Expand Down Expand Up @@ -175,6 +177,7 @@ export const Metric: React.FunctionComponent<{
progressBarSize={progressBarSize}
highContrastTextColor={finalTextColor.keyword}
locale={locale}
fittedValueFontSize={fittedValueFontSize}
/>
{isMetricWTrend(datumWithInteractionColor) && <SparkLine id={metricHTMLId} datum={datumWithInteractionColor} />}
{isMetricWProgress(datumWithInteractionColor) && (
Expand Down
Loading

0 comments on commit 991347d

Please sign in to comment.