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(legend): multiline labels with maxLines option #1285

Merged
merged 16 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@
},
"scripts": {
"prepack": "echo 'This package is not published, see pacakges/*' && exit 1",
"autoprefix:css": "lerna run --loglevel=silent --scope @elastic/charts autoprefix:css --stream",
"api:check": "lerna run --loglevel=silent --scope @elastic/charts api:check --stream",
"api:check:local": "lerna run --loglevel=silent --scope @elastic/charts api:check:local --stream",
"api:extract": "lerna run --loglevel=silent --scope @elastic/charts api:extract --stream",
"autoprefix:css": "lerna run --loglevel=silent --scope @elastic/charts autoprefix:css --stream --no-prefix",
"api:check": "lerna run --loglevel=silent --scope @elastic/charts api:check --stream --no-prefix",
"api:check:local": "lerna run --loglevel=silent --scope @elastic/charts api:check:local --stream --no-prefix",
"api:extract": "lerna run --loglevel=silent --scope @elastic/charts api:extract --stream --no-prefix",
"backport": "backport",
"build": "lerna run --loglevel=silent --scope @elastic/charts build --stream",
"build:ts": "lerna run --loglevel=silent --scope @elastic/charts build:ts --stream",
"build:css": "lerna run --loglevel=silent --scope @elastic/charts build:css --stream",
"build:clean": "lerna run --loglevel=silent --scope @elastic/charts build:clean --stream",
"build:compile": "lerna run --loglevel=silent --scope @elastic/charts build:compile --stream",
"build:sass": "lerna run --loglevel=silent --scope @elastic/charts build:sass --stream",
"build:check": "lerna run --loglevel=silent --scope @elastic/charts build:check --stream",
"build:watch": "lerna run --loglevel=silent --scope @elastic/charts build:watch --stream",
"concat:sass": "lerna run --loglevel=silent --scope @elastic/charts concat:sass --stream",
"build": "lerna run --loglevel=silent --scope @elastic/charts build --stream --no-prefix",
"build:ts": "lerna run --loglevel=silent --scope @elastic/charts build:ts --stream --no-prefix",
"build:css": "lerna run --loglevel=silent --scope @elastic/charts build:css --stream --no-prefix",
"build:clean": "lerna run --loglevel=silent --scope @elastic/charts build:clean --stream --no-prefix",
"build:compile": "lerna run --loglevel=silent --scope @elastic/charts build:compile --stream --no-prefix",
"build:sass": "lerna run --loglevel=silent --scope @elastic/charts build:sass --stream --no-prefix",
"build:check": "lerna run --loglevel=silent --scope @elastic/charts build:check --stream --no-prefix",
"build:watch": "lerna run --loglevel=silent --scope @elastic/charts build:watch --stream --no-prefix",
"concat:sass": "lerna run --loglevel=silent --scope @elastic/charts concat:sass --stream --no-prefix",
"cz": "git-cz",
"lint": "NODE_ENV=production eslint --quiet --ext .tsx,.ts,.js .",
"lint:fix": "yarn lint --fix",
Expand All @@ -36,8 +36,8 @@
"pq": "pretty-quick",
"semantic-release": "semantic-release",
"start": "yarn storybook",
"storybook": "lerna run --scope charts-storybook start --stream",
"storybook:build": "lerna run --scope charts-storybook build --stream",
"storybook": "lerna run --scope charts-storybook start --stream --no-prefix",
"storybook:build": "lerna run --scope charts-storybook build --stream --no-prefix",
"test": "jest --verbose --config jest.config.js",
"test:tz": "yarn test:tz-utc && yarn test:tz-ny && yarn test:tz-jp",
"test:tz-utc": "TZ=UTC jest --verbose --config=jest.tz.config.js",
Expand All @@ -52,7 +52,7 @@
"test:integration:generate:examples": "./scripts/extract_examples.sh",
"test:integration:generate:page": "./scripts/compile_vrt_page.sh",
"test:integration:server": "cd integration/server && RNG_SEED='elastic-charts' webpack serve --content-base integration/server",
"typecheck:src": "lerna run --loglevel=silent --scope @elastic/charts typecheck --stream",
"typecheck:src": "lerna run --loglevel=silent --scope @elastic/charts typecheck --stream --no-prefix",
"typecheck:all": "tsc -p ./tsconfig.json --noEmit",
"ts:prune": "ts-prune",
"link:kibana": "node ./packages/link_kibana"
Expand Down
15 changes: 12 additions & 3 deletions packages/charts/src/components/legend/_legend_item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ $legendItemHeight: #{$euiFontSizeXS * $euiLineHeight};
align-items: center;
position: relative;

> *:not(.background) {
// wrapper is needed to isolate color icon when wrapped or not
.colorWrapper > *:first-of-type {
// euiPopover adds a div with height of 19px otherwise
height: $legendItemHeight; // prevents color dot from shifting
// this prevents color dot from shifting when wrapped
height: $legendItemHeight;
}

> *:not(.background) {
margin-left: $euiSizeXS;

&:last-child:not(.echLegendItem__extra) {
Expand Down Expand Up @@ -43,6 +48,7 @@ $legendItemHeight: #{$euiFontSizeXS * $euiLineHeight};
display: flex;
justify-content: center;
align-items: center;
height: $legendItemHeight;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

makes the focus size of the action the same as the color dot

max-width: calc(#{$legendItemHeight} + #{$euiSizeXS * 2});

.euiPopover,
Expand All @@ -62,13 +68,16 @@ $legendItemHeight: #{$euiFontSizeXS * $euiLineHeight};

&__label {
@include euiFontSizeXS;
@include euiTextTruncate;
flex: 1 1 auto;
text-align: left;
vertical-align: baseline;
letter-spacing: unset;
align-items: center;

&:not(&--multiline) {
@include euiTextTruncate;
}

&--clickable {
&:hover {
cursor: pointer;
Expand Down
8 changes: 6 additions & 2 deletions packages/charts/src/components/legend/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,30 @@
import classNames from 'classnames';
import React, { MouseEventHandler } from 'react';

import { LegendLabelOptions } from '../../utils/themes/theme';

interface LabelProps {
label: string;
isSeriesHidden?: boolean;
isToggleable?: boolean;
onClick?: MouseEventHandler;
options?: LegendLabelOptions;
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: same here, maybe all such optionality like can go away except for the single public API enty point. I admit to having done plenty of this in code I wrote, more recently thinking that it'd be more economical to reify user input right away, and then avoid having to worry about partials, optionals etc.

We can think about removing question marks from non-@public types, TS is great for safely allowing such a transition, in the meantime we can consider avoiding them in new code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done here d8be979

}
/**
* Label component used to display text in legend item
* @internal
*/
export function Label({ label, isToggleable, onClick, isSeriesHidden }: LabelProps) {
export function Label({ label, isToggleable, onClick, isSeriesHidden, options }: LabelProps) {
const labelClassNames = classNames('echLegendItem__label', {
'echLegendItem__label--clickable': Boolean(onClick),
'echLegendItem__label--multiline': options?.multiline,
});

return isToggleable ? (
<button
type="button"
className={labelClassNames}
title={label}
title={options?.multiline ? '' : label} // full text already visible
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think the title is necessary if the full label is visible

onClick={onClick}
aria-label={
isSeriesHidden ? `${label}; Activate to show series in graph` : `${label}; Activate to hide series in graph`
Expand Down
1 change: 1 addition & 0 deletions packages/charts/src/components/legend/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) {
toggleDeselectSeriesAction: props.onToggleDeselectSeriesAction,
colorPicker: config.legendColorPicker,
action: config.legendAction,
labelOptions: legend.labelOptions,
};
const positionStyle = legendPositionStyle(config, size, chartDimensions, containerDimensions);
return (
Expand Down
34 changes: 24 additions & 10 deletions packages/charts/src/components/legend/legend_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '../../state/actions/legend';
import { Color, LayoutDirection } from '../../utils/common';
import { deepEqual } from '../../utils/fast_deep_equal';
import { LegendLabelOptions } from '../../utils/themes/theme';
import { Color as ItemColor } from './color';
import { renderExtra } from './extra';
import { Label as ItemLabel } from './label';
Expand All @@ -45,6 +46,7 @@ export interface LegendItemProps {
positionConfig: LegendPositionConfig;
extraValues: Map<string, LegendItemExtraValues>;
showExtra: boolean;
labelOptions?: LegendLabelOptions;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: could it be relatively easily solved such that it's only the user facing config API where this is optional, but we can rely on its presence everywhere downstream? Ie. one question mark only

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You saying labelOptions: LegendLabelOptions;?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it's an internal type, by this time the reification (augmentation with defaults etc.) of the user input should've happened (if we have shared buy-in of how it often simplifies the implementation and prevents us from possibly handling the non-specified nullish case at multiple places, so also a DRY principle)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done here d8be979

colorPicker?: LegendColorPicker;
action?: LegendAction;
onClick?: LegendItemListener;
Expand Down Expand Up @@ -163,7 +165,16 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
}

render() {
const { extraValues, item, showExtra, colorPicker, totalItems, action: Action, positionConfig } = this.props;
const {
extraValues,
item,
showExtra,
colorPicker,
totalItems,
action: Action,
positionConfig,
labelOptions,
} = this.props;
const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label, pointStyle } = item;

if (isItemHidden) return null;
Expand All @@ -189,17 +200,20 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
data-ech-series-name={label}
>
<div className="background" />
<ItemColor
ref={this.colorRef}
color={color}
seriesName={label}
isSeriesHidden={isSeriesHidden}
hasColorPicker={hasColorPicker}
onClick={this.handleColorClick(hasColorPicker)}
pointStyle={pointStyle}
/>
<div className="colorWrapper">
<ItemColor
ref={this.colorRef}
color={color}
seriesName={label}
isSeriesHidden={isSeriesHidden}
hasColorPicker={hasColorPicker}
onClick={this.handleColorClick(hasColorPicker)}
pointStyle={pointStyle}
/>
</div>
Comment on lines +203 to +213
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is to prevent jitter when using EuiWrappingPopover to display the color picker. Before this was picked using :first-of-type but this is more strict though a little more verbose.

<ItemLabel
label={label}
options={labelOptions}
isToggleable={totalItems > 1 && item.isToggleable}
onClick={this.handleLabelClick(seriesIdentifiers)}
isSeriesHidden={isSeriesHidden}
Expand Down
13 changes: 13 additions & 0 deletions packages/charts/src/utils/themes/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ export interface BackgroundStyle {
*/
color: string;
}

/** @internal */
export interface LegendLabelOptions {
/**
* Allows multiline labels
*/
multiline: boolean;
}

/** @public */
export interface LegendStyle {
/**
Expand All @@ -225,6 +234,10 @@ export interface LegendStyle {
* TODO: make SimplePadding when after axis changes are added
*/
margin: number;
/**
* Options to control legend labels
*/
labelOptions?: LegendLabelOptions;
}
/** @public */
export interface Theme {
Expand Down
21 changes: 16 additions & 5 deletions storybook/stories/legend/11_legend_actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
LegendAction,
XYChartSeriesIdentifier,
LegendColorPicker,
LegendLabelOptions,
} from '@elastic/charts';
import * as TestDatasets from '@elastic/charts/src/utils/data_samples/test_dataset';

Expand Down Expand Up @@ -159,17 +160,27 @@ export const renderEuiColorPicker = (anchorPosition: PopoverAnchorPosition): Leg
</EuiWrappingPopover>
);

const getLabelOptionKnobs = (): LegendLabelOptions => {
const group = 'Label options';

return {
multiline: boolean('Multiline', false, group),
};
};

export const Example = () => {
const hideActions = boolean('Hide legend action', false);
const showLegendExtra = !boolean('Hide legend extra', false);
const showColorPicker = !boolean('Hide color picker', true);
const legendPosition = getPositionKnob('Legend position');
const euiPopoverPosition = getEuiPopoverPositionKnob();
const hideActions = boolean('Hide legend action', false, 'Legend');
const showLegendExtra = !boolean('Hide legend extra', false, 'Legend');
const showColorPicker = !boolean('Hide color picker', true, 'Legend');
const legendPosition = getPositionKnob('Legend position', undefined, 'Legend');
const euiPopoverPosition = getEuiPopoverPositionKnob(undefined, undefined, 'Legend');
const labelOptions = getLabelOptionKnobs();

return (
<Chart>
<Settings
showLegend
theme={{ legend: { labelOptions } }}
baseTheme={useBaseTheme()}
showLegendExtra={showLegendExtra}
legendPosition={legendPosition}
Expand Down
5 changes: 4 additions & 1 deletion storybook/stories/utils/knobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const getKnobsFromEnum = <T extends SelectTypeKnobValue, O extends Record
group,
) || undefined;

export const getPositionKnob = (name = 'chartRotation', defaultValue: Position = Position.Right) =>
export const getPositionKnob = (name = 'chartRotation', defaultValue: Position = Position.Right, group?: string) =>
select<Position>(
name,
{
Expand All @@ -111,6 +111,7 @@ export const getPositionKnob = (name = 'chartRotation', defaultValue: Position =
Bottom: Position.Bottom,
},
defaultValue,
group,
);

export const getPlacementKnob = (name = 'placement', defaultValue?: Placement, groupId?: string) => {
Expand Down Expand Up @@ -158,6 +159,7 @@ export const getStickToKnob = (name = 'stickTo', defaultValue = TooltipStickTo.M
export const getEuiPopoverPositionKnob = (
name = 'Popover position',
defaultValue: PopoverAnchorPosition = 'leftCenter',
group?: string,
) =>
select<PopoverAnchorPosition>(
name,
Expand All @@ -176,6 +178,7 @@ export const getEuiPopoverPositionKnob = (
rightDown: 'rightDown',
},
defaultValue,
group,
);

export function arrayKnobs(name: string, values: (string | number)[]): (string | number)[] {
Expand Down