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): make value fit logic uniform across panels #2484

Merged
merged 4 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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