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): Metric visualization #1658

Merged
merged 35 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0b3265e
Basic chart template
markov00 Apr 20, 2022
0c2357a
feat(new_chart): metric
markov00 May 19, 2022
2b27347
Merge branch 'master' into single_metric
markov00 May 19, 2022
7060f59
test: add vrt test for dark mode and other config
markov00 May 19, 2022
d939d4e
test: add screnshots for added VRTs
markov00 May 19, 2022
51e5977
Merge branch 'master' into single_metric
markov00 May 20, 2022
be80e67
fix: run prettier on added scss
markov00 May 27, 2022
5dc1fcf
Merge branch 'master' into single_metric
markov00 May 27, 2022
bb06992
fix: use Metric type instead of Goal
markov00 Jun 6, 2022
8768f0f
refactor: DRY getSpecs method
markov00 Jun 6, 2022
4e72633
fix: empty description for now
markov00 Jun 6, 2022
e538ac0
refactor: rename style barBg to barBackground
markov00 Jun 6, 2022
bc406cf
refactor: simplify Metric component generation
markov00 Jun 6, 2022
e9842b4
docs: update APIs
markov00 Jun 6, 2022
8777acd
style: align sizes to BEM
markov00 Jun 6, 2022
ea28f38
style: use 8px margin
markov00 Jun 6, 2022
2213696
refactor: use an explicit min, max domain
markov00 Jun 6, 2022
14805b6
test: uprate VRT
markov00 Jun 6, 2022
84f9802
test: update VRTs
markov00 Jun 6, 2022
c70c557
Merge branch 'master' into single_metric
markov00 Jun 6, 2022
a0df3bc
a11y: improve a11y
markov00 Jun 6, 2022
897a664
fix: prettier...
markov00 Jun 7, 2022
5d398b4
refactor: moved and improved the resposivness of the chart
markov00 Jun 7, 2022
fc03e21
test: create single metric and grid stories
markov00 Jun 8, 2022
3c080b1
test: update VRTs
markov00 Jun 8, 2022
6d011a8
api: update doc
markov00 Jun 8, 2022
fc91ca4
feat: shape type and extra in storybook
markov00 Jun 8, 2022
b2abab6
Update vrts
markov00 Jun 8, 2022
4784459
fix: show full color when progress is none
markov00 Jun 8, 2022
b34deef
refactor: simplified specs and use progress bar direction only for pr…
markov00 Jun 8, 2022
cfd1e3b
test: update VRTs
markov00 Jun 8, 2022
55da629
fix: expose TrendShape as enum
markov00 Jun 9, 2022
5f57e2b
refactor: less noise on chart state
markov00 Jun 9, 2022
031a232
refactor: find ranges with single expression
markov00 Jun 9, 2022
b09c05f
Merge branch 'master' into single_metric
markov00 Jun 10, 2022
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.
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.
10 changes: 5 additions & 5 deletions integration/tests/metric_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ import { common } from '../page_objects';
describe('Metric', () => {
it('should render horizontal progress bar', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:light&knob-progress bar=small&knob-progress bar orientation=horizontal&knob-orientation=grid',
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:light&knob-use progress bar=true&knob-progress bar direction=horizontal&knob-max trend data points=30&knob-layout=grid',
);
});
it('should render no progress bar', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:light&knob-progress bar=none&knob-progress bar orientation=vertical&knob-orientation=grid',
'http://localhost:9001/?path=/story/metric-alpha--grid&globals=theme:light&knob-use progress bar=&knob-progress bar direction=horizontal&knob-max trend data points=30&knob-layout=grid',
);
});
it('should render vertical progress bar in dark mode', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-dark&knob-orientation=grid&knob-progress bar=small&knob-progress bar orientation=vertical',
'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',
);
});
it('should render horizontal progress bar in dark mode', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-dark&knob-orientation=grid&knob-progress bar=small&knob-progress bar orientation=horizontal',
'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',
);
});
it('should render no progress bar in dark mode', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/metric-alpha--basic&globals=theme:eui-dark&knob-orientation=grid&knob-progress bar=none&knob-progress bar orientation=horizontal',
'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=',
);
});
});
32 changes: 12 additions & 20 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,7 @@ export function getNodeName(node: ArrayNode): string;
// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts
//
// @alpha
export const Goal: (props: SFProps<GoalSpec, keyof typeof buildProps_2['overrides'], keyof typeof buildProps_2['defaults'], keyof typeof buildProps_2['optionals'], keyof typeof buildProps_2['requires']>) => null;
export const Goal: (props: SFProps<GoalSpec, keyof typeof buildProps['overrides'], keyof typeof buildProps['defaults'], keyof typeof buildProps['optionals'], keyof typeof buildProps['requires']>) => null;

// @alpha (undocumented)
export interface GoalDomainRange {
Expand Down Expand Up @@ -1650,10 +1650,8 @@ export function mergeWithDefaultAnnotationRect(config?: RecursivePartial<RectAnn
// @public @deprecated
export function mergeWithDefaultTheme(theme: PartialTheme, defaultTheme?: Theme, auxiliaryThemes?: PartialTheme[]): Theme;

// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts
//
// @alpha
export const Metric: (props: SFProps<MetricSpec, keyof typeof buildProps['overrides'], keyof typeof buildProps['defaults'], keyof typeof buildProps['optionals'], keyof typeof buildProps['requires']>) => null;
// @alpha (undocumented)
export const Metric: FC<SFProps<MetricSpec, "chartType" | "specType", "data", never, "id">>;

// @alpha (undocumented)
export type MetricBase = {
Expand All @@ -1672,10 +1670,6 @@ export interface MetricSpec extends Spec {
// (undocumented)
data: (MetricBase | MetricWProgress | MetricWTrend | undefined)[][];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can MetricSpec be generic on the use case? Ternaries have some extra cost compared to generics. Also, why do we need to admit undefined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • MetricSpec with generic: with the current config no. Because you can mix a simple metric with a static background color with one with a trend and with another with a progress bar.

  • why undefined because we allow gaps between metrics in a row/column.

// (undocumented)
progressBarMode: ProgressBarMode;
// (undocumented)
progressBarOrientation?: LayoutDirection;
// (undocumented)
specType: typeof SpecType.Series;
}

Expand All @@ -1687,7 +1681,7 @@ export interface MetricStyle {
// (undocumented)
background: Color;
// (undocumented)
barBg: Color;
barBackground: Color;
// (undocumented)
nonFiniteText: string;
// (undocumented)
Expand All @@ -1699,7 +1693,11 @@ export interface MetricStyle {

// @alpha (undocumented)
export type MetricWProgress = MetricBase & {
domain: [min: number, max: number];
domain: {
min: number;
max: number;
};
progressBarDirection?: LayoutDirection;
};

// @alpha (undocumented)
Expand All @@ -1708,6 +1706,9 @@ export type MetricWTrend = MetricBase & {
x: number;
y: number;
}[];
trendShape?: 'area' | 'bar';
trendA11yTitle?: string;
trendA11yDescription?: string;
};

// @public (undocumented)
Expand Down Expand Up @@ -2027,15 +2028,6 @@ export type Predicate = $Values<typeof Predicate>;
// @public
export type PrimitiveValue = string | number | null;

// @alpha (undocumented)
export const ProgressBarMode: Readonly<{
None: "none";
Small: "small";
}>;

// @alpha (undocumented)
export type ProgressBarMode = $Values<typeof ProgressBarMode>;

// @public
export type ProjectedValues = {
x: PrimitiveValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

.echMetric {
position: relative;
overflow: hidden;

&--rightBorder {
border-right: 1px solid #343741;
nickofthyme marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
38 changes: 15 additions & 23 deletions packages/charts/src/chart_types/metric/renderer/dom/_text.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,38 @@

.echMetricText {
position: relative;
padding: 10px;
padding: 8px;
height: 100%;
z-index: 1;
display: grid;
grid-template-columns: 100%;
grid-template-rows: min-content min-content auto min-content min-content;
height: 100%;

&__title {
font-weight: 500;
@include lineClamp(3);
}
&__titleS {
font-size: 16px;
}
&__titleM {
font-size: 24px;
font-weight: 400;
}

&__subtitle {
padding-top: 5px;
font-size: 14px;
font-weight: 300;
@include lineClamp(2);
}

&__extra {
text-align: right;
font-weight: 400;
}

&__value {
position: relative;
font-weight: 600;
text-align: right;
white-space: nowrap;
}
&__valueS {
font-size: 32px;
}
&__valueM {
font-size: 48px;
}
&__unit {
font-size: 14px;

&__part {
font-weight: 400;
}
&__extra {
text-align: right;
}

&__gap {
position: relative;
}
Expand Down
63 changes: 24 additions & 39 deletions packages/charts/src/chart_types/metric/renderer/dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/
import { LayoutDirection } from '../../../../utils/common';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';
import { MetricStyle } from '../../../../utils/themes/theme';
import { isMetricWProgress, isMetricWTrend, MetricSpec, ProgressBarMode } from '../../specs';
import { isMetricWProgress, isMetricWTrend, MetricSpec } from '../../specs';
import { chartSize } from '../../state/selectors/chart_size';
import { getSpecs } from '../../state/selectors/data';
import { getMetricSpecs } from '../../state/selectors/data';
import { ProgressBar } from './progress';
import { SparkLine } from './sparkline';
import { MetricText } from './text';

interface ReactiveChartStateProps {
initialized: boolean;
chartId: string;
size: {
width: number;
height: number;
Expand Down Expand Up @@ -66,6 +67,7 @@ class Component extends React.Component<Props> {

render() {
const {
chartId,
initialized,
size: { width, height },
a11y,
Expand All @@ -76,7 +78,7 @@ class Component extends React.Component<Props> {
return null;
}
// ignoring other specs
const { data, progressBarMode, progressBarOrientation } = specs[0];
const { data } = specs[0];

const maxRows = data.length;
const maxColumns = data.reduce((acc, curr) => {
Expand All @@ -90,14 +92,15 @@ class Component extends React.Component<Props> {
aria-labelledby={a11y.labelId}
aria-describedby={a11y.descriptionId}
style={{
gridTemplateColumns: `repeat(${maxColumns}, minmax(180px, 1fr)`,
gridTemplateRows: `repeat(${maxRows}, minmax(100px, 1fr)`,
gridTemplateColumns: `repeat(${maxColumns}, minmax(0, 1fr)`,
gridTemplateRows: `repeat(${maxRows}, minmax(64px, 1fr)`,
}}
>
{data
.map((columns, ri) => {
return [
...columns.map((d, ci) => {
const metricHTMLId = `echMetric-${chartId}-${ri}-${ci}`;
// fill undefined with empty panels
const emptyMetricClassName = classNames('echMetric', {
'echMetric--rightBorder': ci < maxColumns - 1,
Expand All @@ -106,47 +109,27 @@ class Component extends React.Component<Props> {
if (!d) {
return <div key={`empty-${ci}`} className={emptyMetricClassName}></div>;
}

const hasProgressBar = isMetricWProgress(d);
const progressBarDirection = isMetricWProgress(d) ? d.progressBarDirection : undefined;
const metricPanelClassName = classNames(emptyMetricClassName, {
'echMetric--small': progressBarMode === ProgressBarMode.Small,
'echMetric--vertical': progressBarOrientation === LayoutDirection.Vertical,
'echMetric--horizontal': progressBarOrientation === LayoutDirection.Horizontal,
'echMetric--small': hasProgressBar,
'echMetric--vertical': progressBarDirection === LayoutDirection.Vertical,
'echMetric--horizontal': progressBarDirection === LayoutDirection.Horizontal,
});

return (
<div
role="figure"
aria-labelledby={d.title && metricHTMLId}
key={`${d.title}${d.subtitle}${d.color}${ci}`}
className={metricPanelClassName}
style={{ backgroundColor: style.background }}
style={{
backgroundColor: !isMetricWTrend(d) && !isMetricWProgress(d) ? d.color : style.background,
}}
>
{isMetricWTrend(d) && <SparkLine datum={d} curve="linear" />}
{isMetricWProgress(d) && progressBarMode !== ProgressBarMode.None && (
<ProgressBar
mode={progressBarMode}
orientation={progressBarOrientation}
datum={d}
barBg={style.barBg}
/>
)}
{isMetricWProgress(d) && progressBarMode === ProgressBarMode.None && (
<ProgressBar
mode={progressBarMode}
orientation={progressBarOrientation}
barBg={style.barBg}
datum={{
value: 100,
domain: [100, 100],
color: d.color,
}}
/>
)}
<MetricText
datum={d}
panel={panel}
style={style}
progressBarMode={progressBarMode}
progressBarOrientation={progressBarOrientation}
/>
<MetricText id={metricHTMLId} datum={d} panel={panel} style={style} />
{isMetricWTrend(d) && <SparkLine id={metricHTMLId} datum={d} />}
{isMetricWProgress(d) && <ProgressBar datum={d} barBackground={style.barBackground} />}
</div>
);
}),
Expand Down Expand Up @@ -176,6 +159,7 @@ const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps =>

const DEFAULT_PROPS: ReactiveChartStateProps = {
initialized: false,
chartId: '',
specs: [],
size: {
width: 0,
Expand All @@ -190,7 +174,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
}
return {
initialized: true,
specs: getSpecs(state),
chartId: state.chartId,
specs: getMetricSpecs(state),
size: chartSize(state),
a11y: getA11ySettingsSelector(state),
style: getChartThemeSelector(state).metric,
Expand Down
39 changes: 23 additions & 16 deletions packages/charts/src/chart_types/metric/renderer/dom/progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,40 @@ import React from 'react';

import { Color } from '../../../../common/colors';
import { clamp, LayoutDirection } from '../../../../utils/common';
import { MetricWProgress, MetricSpec, ProgressBarMode } from '../../specs';
import { MetricWProgress } from '../../specs';

/** @internal */
export const ProgressBar: React.FunctionComponent<{
datum: Pick<MetricWProgress, 'domain' | 'value' | 'color'>;
mode: MetricSpec['progressBarMode'];
orientation: MetricSpec['progressBarOrientation'];
barBg: Color;
}> = ({ datum: { domain, value, color }, mode, orientation, barBg }) => {
const isVertical = orientation === LayoutDirection.Vertical;
const isSmall = mode === ProgressBarMode.Small;
const percent = clamp((domain ? value / (domain[1] - domain[0]) : 1) * 100, 0, 100);
datum: Pick<MetricWProgress, 'title' | 'domain' | 'value' | 'color' | 'progressBarDirection'>;
barBackground: Color;
}> = ({ datum: { title, domain, value, color, progressBarDirection }, barBackground }) => {
const verticalDirection = progressBarDirection === LayoutDirection.Vertical;
// currently we provide only the small progress bar;
const isSmall = true;
const percent = Number(clamp((domain ? value / (domain.max - domain.min) : 1) * 100, 0, 100).toFixed(2));

const bgClassName = classNames('echSingleMetricProgress', {
'echSingleMetricProgress--vertical': isVertical,
'echSingleMetricProgress--horizontal': !isVertical,
'echSingleMetricProgress--vertical': verticalDirection,
'echSingleMetricProgress--horizontal': !verticalDirection,
'echSingleMetricProgress--small': isSmall,
});
const barClassName = classNames('echSingleMetricProgressBar', {
'echSingleMetricProgressBar--vertical': isVertical,
'echSingleMetricProgressBar--horizontal': !isVertical,
'echSingleMetricProgressBar--vertical': verticalDirection,
'echSingleMetricProgressBar--horizontal': !verticalDirection,
'echSingleMetricProgressBar--small': isSmall,
});
const percentProp = isVertical ? { height: `${percent}%` } : { width: `${percent}%` };
const percentProp = verticalDirection ? { height: `${percent}%` } : { width: `${percent}%` };
return (
<div className={bgClassName} style={{ background: isSmall ? barBg : undefined }}>
<div className={barClassName} style={{ background: color, ...percentProp }} />
<div className={bgClassName} style={{ background: isSmall ? barBackground : undefined }}>
<div
className={barClassName}
style={{ background: color, ...percentProp }}
role="meter"
aria-label={title ? `Percentage of ${title}` : 'Percentage'}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percent}
/>
</div>
);
};
Loading