Skip to content

Commit

Permalink
feat(legend/click): add click interations on legend titles (opensearc…
Browse files Browse the repository at this point in the history
  • Loading branch information
emmacunningham committed Mar 6, 2019
1 parent ccd3ab0 commit 6d756e4
Show file tree
Hide file tree
Showing 17 changed files with 878 additions and 51 deletions.
12 changes: 11 additions & 1 deletion packages/osd-charts/src/components/_legend.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,24 @@ $elasticChartsLegendMaxHeight: $euiSize * 4;
}

.elasticChartsLegendList__item {
cursor: pointer;

&:hover {
text-decoration: underline;
.elasticChartsLegendListItem__title {
text-decoration: underline;
}
}
}

.elasticChartsLegendListItem__title {
width: $elasticChartsLegendMaxWidth - 4 * $euiSize;
max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize;

&.elasticChartsLegendListItem__title--selected {
.elasticChartsLegendListItem__title {
text-decoration: underline;
}
}
}

.elasticChartsLegend__toggle {
Expand Down
32 changes: 14 additions & 18 deletions packages/osd-charts/src/components/legend.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { isVertical } from '../lib/axes/axis_utils';
import { LegendItem } from '../lib/series/legend';
import { ChartStore } from '../state/chart_state';
import { LegendElement } from './legend_element';

interface ReactiveChartProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
Expand Down Expand Up @@ -74,9 +78,11 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
onMouseLeave: this.onLegendItemMouseout,
};

const { color, label, isVisible } = item;

return (
<EuiFlexItem {...legendItemProps}>
<LegendElement color={item.color} label={item.label} />
{this.renderLegendElement({ color, label, isVisible }, index)}
</EuiFlexItem>
);
})}
Expand All @@ -93,22 +99,12 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
private onLegendItemMouseout = () => {
this.props.chartStore!.onLegendItemOut();
}
}
function LegendElement({ color, label }: Partial<LegendItem>) {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="dot" color={color} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={true} className="elasticChartsLegendListItem__title" title={label}>
<EuiText size="xs" className="eui-textTruncate">
{label}
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
</EuiFlexGroup>
);

private renderLegendElement = ({ color, label, isVisible }: Partial<LegendItem>, legendItemIndex: number) => {
const props = { color, label, isVisible, index: legendItemIndex };

return <LegendElement {...props} />;
}
}

export const Legend = inject('chartStore')(observer(LegendComponent));
168 changes: 168 additions & 0 deletions packages/osd-charts/src/components/legend_element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
EuiButtonIcon,
// TODO: remove ts-ignore below once typings file is included in eui for color picker
// @ts-ignore
EuiColorPicker,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
EuiText,
} from '@elastic/eui';
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';

import { ChartStore } from '../state/chart_state';

interface LegendElementProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
index: number;
color: string | undefined;
label: string | undefined;
isVisible?: boolean;
}

interface LegendElementState {
isColorPickerOpen: boolean;
}

class LegendElementComponent extends React.Component<LegendElementProps, LegendElementState> {
static displayName = 'LegendElement';

constructor(props: LegendElementProps) {
super(props);
this.state = {
isColorPickerOpen: false,
};
}

closeColorPicker = () => {
this.setState({
isColorPickerOpen: false,
});
}

toggleColorPicker = () => {
this.setState({
isColorPickerOpen: !this.state.isColorPickerOpen,
});
}

render() {
const legendItemIndex = this.props.index;
const { color, label, isVisible } = this.props;

const onTitleClick = this.onLegendTitleClick(legendItemIndex);

const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get();
const titleClassNames = classNames({
['elasticChartsLegendListItem__title--selected']: isSelected,
}, 'elasticChartsLegendListItem__title');

const colorDotProps = {
color,
onClick: this.toggleColorPicker,
};

const colorDot = <EuiIcon type="dot" {...colorDotProps} />;

return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiPopover
id="legendItemColorPicker"
button={colorDot}
isOpen={this.state.isColorPickerOpen}
closePopover={this.closeColorPicker}
panelPaddingSize="s"
anchorPosition="downCenter"
>
<EuiContextMenuPanel>
<EuiColorPicker onChange={this.onColorPickerChange(legendItemIndex)} color={color} />
</EuiContextMenuPanel>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{this.renderVisibilityButton(legendItemIndex, isVisible)}
</EuiFlexItem>
<EuiFlexItem grow={false} className={titleClassNames} onClick={onTitleClick}>
<EuiPopover
id="contentPanel"
button={(<EuiText size="xs" className="eui-textTruncate elasticChartsLegendListItem__title">
{label}
</EuiText>)
}
isOpen={isSelected}
closePopover={this.onLegendItemPanelClose}
panelPaddingSize="s"
anchorPosition="downCenter"
>
<EuiContextMenuPanel>
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem>
{this.renderPlusButton()}
</EuiFlexItem>
<EuiFlexItem>
{this.renderMinusButton()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiContextMenuPanel>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
}

private onLegendTitleClick = (legendItemIndex: number) => () => {
this.props.chartStore!.onLegendItemClick(legendItemIndex);
}

private onLegendItemPanelClose = () => {
// tslint:disable-next-line:no-console
console.log('close');
}

private onColorPickerChange = (legendItemIndex: number) => (color: string) => {
this.props.chartStore!.setSeriesColor(legendItemIndex, color);
}

private renderPlusButton = () => {
return (
<EuiButtonIcon
onClick={this.props.chartStore!.onLegendItemPlusClick}
iconType="plusInCircle"
aria-label="minus"
/>);
}

private renderMinusButton = () => {
return (
<EuiButtonIcon
onClick={this.props.chartStore!.onLegendItemMinusClick}
iconType="minusInCircle"
aria-label="minus"
/>);
}

private onVisibilityClick = (legendItemIndex: number) => (event: React.MouseEvent<HTMLElement>) => {
if (event.shiftKey) {
this.props.chartStore!.toggleSingleSeries(legendItemIndex);
} else {
this.props.chartStore!.toggleSeriesVisibility(legendItemIndex);
}
}

private renderVisibilityButton = (legendItemIndex: number, isVisible: boolean = true) => {
const iconType = isVisible ? 'eye' : 'eyeClosed';

return <EuiButtonIcon
onClick={this.onVisibilityClick(legendItemIndex)}
iconType={iconType}
aria-label="toggle visibility"
/>;
}
}

export const LegendElement = inject('chartStore')(observer(LegendElementComponent));
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface LineGeometriesDataState {
export class LineGeometries extends React.PureComponent<
LineGeometriesDataProps,
LineGeometriesDataState
> {
> {
static defaultProps: Partial<LineGeometriesDataProps> = {
animated: false,
};
Expand All @@ -41,6 +41,7 @@ export class LineGeometries extends React.PureComponent<
overPoint: undefined,
};
}

render() {
return (
<Group ref={this.barSeriesRef} key={'bar_series'}>
Expand Down Expand Up @@ -153,11 +154,12 @@ export class LineGeometries extends React.PureComponent<
if (this.props.animated) {
return (
<Group key={i} x={transform.x}>
<Spring native from={{ line }} to={{ line }}>
{(props: { line: string }) => (
<Spring native reset from={{ opacity: 0 }} to={{ opacity: 1 }}>
{(props: { opacity: number }) => (
<animated.Path
opacity={props.opacity}
key="line"
data={props.line}
data={line}
strokeWidth={strokeWidth}
stroke={color}
listening={false}
Expand All @@ -172,7 +174,7 @@ export class LineGeometries extends React.PureComponent<
} else {
return (
<Path
key="line"
key={i}
data={line}
strokeWidth={strokeWidth}
stroke={color}
Expand Down
41 changes: 35 additions & 6 deletions packages/osd-charts/src/lib/series/legend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('Legends', () => {
seriesColor.set('colorSeries1a', colorValues1a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -69,8 +69,8 @@ describe('Legends', () => {
seriesColor.set('colorSeries1b', colorValues1b);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
{ color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -79,8 +79,8 @@ describe('Legends', () => {
seriesColor.set('colorSeries2a', colorValues2a);
const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet');
const expected = [
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' } },
{ color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
{ color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
Expand All @@ -94,8 +94,37 @@ describe('Legends', () => {
const emptyColorMap = new Map<string, string>();
const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet');
const expected = [
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' } },
{ color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true },
];
expect(legend).toEqual(expected);
});
it('sets all series legend items to visible when selectedDataSeries is null', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries1b', colorValues1b);
seriesColor.set('colorSeries2a', colorValues2a);
seriesColor.set('colorSeries2b', colorValues2b);

const emptyColorMap = new Map<string, string>();
const selectedDataSeries = null;

const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);

const visibility = legend.map((item) => item.isVisible);

expect(visibility).toEqual([true, true, true, true]);
});
it('selectively sets series to visible when there are selectedDataSeries items', () => {
seriesColor.set('colorSeries1a', colorValues1a);
seriesColor.set('colorSeries1b', colorValues1b);
seriesColor.set('colorSeries2a', colorValues2a);
seriesColor.set('colorSeries2b', colorValues2b);

const emptyColorMap = new Map<string, string>();
const selectedDataSeries = [colorValues1a, colorValues1b];

const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries);

const visibility = legend.map((item) => item.isVisible);
expect(visibility).toEqual([true, true, false, false]);
});
});
Loading

0 comments on commit 6d756e4

Please sign in to comment.