Skip to content

Commit

Permalink
feat(legend): add custom legend component (#1889)
Browse files Browse the repository at this point in the history
The settings now have a new prop `customLegend` which lets users provide there own legend React component that is rendered with the chart state into the legend container.
Analogously to the `customTooltip` in the `Tooltip`, the `customLegend` takes a React component which will receive the state of the standard legend as props. This allows users to provide their own legend implementation. The legend is still rendered inside of the `div.echLegendListContainer`.

Co-authored-by: Marco Vettorello <[email protected]>
  • Loading branch information
yannbolliger and markov00 authored Dec 19, 2022
1 parent 2ba2911 commit 2e1648d
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 30 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 36 additions & 5 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,9 @@ export interface BubbleSeriesStyle {
// @public (undocumented)
export type CategoryKey = string;

// @public (undocumented)
export type CategoryLabel = string;

// @public (undocumented)
export interface Cell {
// Warning: (ae-forgotten-export) The symbol "HeatmapCellDatum" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -644,6 +647,29 @@ export type CustomAnnotationTooltip = ComponentType<{
datum: LineAnnotationDatum | RectAnnotationDatum;
}> | null;

// @public
export type CustomLegend = ComponentType<CustomLegendProps>;

// @public
export interface CustomLegendProps {
// (undocumented)
items: {
seriesIdentifiers: SeriesIdentifier[];
path: LegendPath;
color: Color;
label: CategoryLabel;
seriesType?: SeriesType;
pointStyle?: PointStyle;
extraValue?: PrimitiveValue;
isSeriesHidden?: boolean;
onItemOverActon: () => void;
onItemOutAction: () => void;
onItemClickAction: (negate: boolean) => void;
}[];
// (undocumented)
pointerValue?: PointerValue;
}

// @public
export type CustomTooltip<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> = ComponentType<CustomTooltipProps<D, SI>>;

Expand Down Expand Up @@ -1582,6 +1608,7 @@ export type LegendPositionConfig = {

// @public
export interface LegendSpec {
customLegend?: CustomLegend;
flatLegend?: boolean;
legendAction?: LegendAction;
// (undocumented)
Expand Down Expand Up @@ -2097,6 +2124,13 @@ export const PointerUpdateTrigger: Readonly<{
// @public (undocumented)
export type PointerUpdateTrigger = $Values<typeof PointerUpdateTrigger>;

// @public
export interface PointerValue<D extends BaseDatum = Datum> {
formattedValue: string;
value: any;
valueAccessor?: Accessor<D>;
}

// @public (undocumented)
export const PointShape: Readonly<{
Circle: "circle";
Expand Down Expand Up @@ -2445,7 +2479,7 @@ export const Settings: (props: SFProps<SettingsSpec, keyof (typeof settingsBuild
// Warning: (ae-forgotten-export) The symbol "BuildProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "rotation" | "debug" | "legendPosition" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary", "ariaLabel" | "tooltip" | "theme" | "onBrushEnd" | "flatLegend" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "xDomain" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "ariaLabelledBy" | "ariaDescription" | "ariaDescribedBy" | "ariaTableCaption" | "legendSort", never>;
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "rotation" | "debug" | "legendPosition" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary", "ariaLabel" | "tooltip" | "theme" | "xDomain" | "onBrushEnd" | "flatLegend" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "customLegend" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "ariaLabelledBy" | "ariaDescription" | "ariaDescribedBy" | "ariaTableCaption" | "legendSort", never>;

// @public (undocumented)
export type SettingsProps = ComponentProps<typeof Settings>;
Expand Down Expand Up @@ -3006,18 +3040,15 @@ export const TooltipType: Readonly<{
export type TooltipType = $Values<typeof TooltipType>;

// @public
export interface TooltipValue<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> {
export interface TooltipValue<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> extends PointerValue<D> {
color: Color;
datum?: D;
formattedMarkValue?: string | null;
formattedValue: string;
isHighlighted: boolean;
isVisible: boolean;
label: string;
markValue?: number | null;
seriesIdentifier: SI;
value: any;
valueAccessor?: Accessor<D>;
}

// @public
Expand Down
2 changes: 1 addition & 1 deletion packages/charts/src/common/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
/** @public */
export type CategoryKey = string;

/** @internal */
/** @public */
export type CategoryLabel = string;
27 changes: 27 additions & 0 deletions packages/charts/src/components/legend/custom_legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { connect } from 'react-redux';

import { CustomLegendProps, CustomLegend as CustomLegendComponent } from '../../specs';
import { GlobalChartState } from '../../state/chart_state';
import { getPointerValueSelector } from '../../state/selectors/get_pointer_value';

interface Props extends CustomLegendProps {
component: CustomLegendComponent;
}

const CustomLegendComponent: React.FC<Props> = ({ component: Component, ...props }) => <Component {...props} />;

const mapStateToProps = (state: GlobalChartState) => ({
pointerValue: getPointerValueSelector(state),
});

/** @internal */
export const CustomLegend = connect(mapStateToProps)(CustomLegendComponent);
28 changes: 23 additions & 5 deletions packages/charts/src/components/legend/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { hasMostlyRTLItems, HorizontalAlignment, LayoutDirection, VerticalAlignm
import { Dimensions, Size } from '../../utils/dimensions';
import { LIGHT_THEME } from '../../utils/themes/light_theme';
import { Theme } from '../../utils/themes/theme';
import { CustomLegend } from './custom_legend';
import { LegendItemProps, renderLegendItem } from './legend_item';
import { getLegendPositionConfig, legendPositionStyle } from './position_style';
import { getLegendStyle, getLegendListStyle } from './style_utils';
Expand Down Expand Up @@ -112,11 +113,28 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) {
const positionStyle = legendPositionStyle(config, size, chartDimensions, containerDimensions);
return (
<div className={legendClasses} style={positionStyle} dir={isMostlyRTL ? 'rtl' : 'ltr'}>
<div style={containerStyle} className="echLegendListContainer">
<ul style={listStyle} className="echLegendList">
{items.map((item, index) => renderLegendItem(item, itemProps, index))}
</ul>
</div>
{config.customLegend ? (
<div style={containerStyle}>
<CustomLegend
component={config.customLegend}
items={items.map(({ seriesIdentifiers, childId, path, ...customProps }) => ({
...customProps,
seriesIdentifiers,
path,
extraValue: itemProps.extraValues.get(seriesIdentifiers[0].key)?.get(childId || ''),
onItemOutAction: itemProps.mouseOutAction,
onItemOverActon: () => itemProps.mouseOverAction(path),
onItemClickAction: (negate: boolean) => itemProps.toggleDeselectSeriesAction(seriesIdentifiers, negate),
}))}
/>
</div>
) : (
<div style={containerStyle} className="echLegendListContainer">
<ul style={listStyle} className="echLegendList">
{items.map((item, index) => renderLegendItem(item, itemProps, index))}
</ul>
</div>
)}
</div>
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/charts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
DebugStateAxis,
DebugStateBase,
DebugStateLegendItem,
PointerValue,
} from './state/types';
export { toEntries } from './utils/common';
export { CurveType } from './utils/curves';
Expand All @@ -43,7 +44,7 @@ export {
} from './chart_types/xy_chart/annotations/types';
export { GeometryValue, BandedAccessorType } from './utils/geometry';
export { LegendPath, LegendPathElement } from './state/actions/legend';
export { CategoryKey } from './common/category';
export { CategoryKey, CategoryLabel } from './common/category';
export { Layer as PartitionLayer, PartitionProps } from './chart_types/partition_chart/specs/index';
export { FillLabelConfig as PartitionFillLabel, PartitionStyle } from './utils/themes/partition';
export { PartitionLayout } from './chart_types/partition_chart/layout/types/config_types';
Expand Down
37 changes: 35 additions & 2 deletions packages/charts/src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import { CustomXDomain, GroupByAccessor, Spec } from '.';
import { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types';
import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup';
import { LegendStrategy } from '../chart_types/partition_chart/layout/utils/highlighted_geoms';
import { LineAnnotationDatum, RectAnnotationDatum } from '../chart_types/specs';
import { LineAnnotationDatum, RectAnnotationDatum, SeriesType } from '../chart_types/specs';
import { WordModel } from '../chart_types/wordcloud/layout/types/viewmodel_types';
import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series';
import { CategoryLabel } from '../common/category';
import { Color } from '../common/colors';
import { SeriesIdentifier } from '../common/series_id';
import { TooltipPortalSettings } from '../components';
import { ScaleContinuousType, ScaleOrdinalType } from '../scales';
import { LegendPath } from '../state/actions/legend';
import { SFProps, useSpecFactory } from '../state/spec_factory';
import { PointerValue } from '../state/types';
import {
HorizontalAlignment,
LayoutDirection,
Expand All @@ -34,7 +36,7 @@ import { Dimensions } from '../utils/dimensions';
import { GeometryValue } from '../utils/geometry';
import { GroupId, SpecId } from '../utils/ids';
import { SeriesCompareFn } from '../utils/series_sort';
import { PartialTheme, Theme } from '../utils/themes/theme';
import { PartialTheme, PointStyle, Theme } from '../utils/themes/theme';
import { BinAgg, BrushAxis, Direction, PointerEventType, PointerUpdateTrigger, settingsBuildProps } from './constants';
import { TooltipSettings } from './tooltip';

Expand Down Expand Up @@ -365,6 +367,33 @@ export type LegendPositionConfig = {
// TODO add grow factor: fill, shrink, fixed column size
};

/**
* The props for {@link CustomLegend}
* @public
*/
export interface CustomLegendProps {
pointerValue?: PointerValue;
items: {
seriesIdentifiers: SeriesIdentifier[];
path: LegendPath;
color: Color;
label: CategoryLabel;
seriesType?: SeriesType;
pointStyle?: PointStyle;
extraValue?: PrimitiveValue;
isSeriesHidden?: boolean;
onItemOverActon: () => void;
onItemOutAction: () => void;
onItemClickAction: (negate: boolean) => void;
}[];
}

/**
* The react component used to render a custom legend
* @public
*/
export type CustomLegend = ComponentType<CustomLegendProps>;

/**
* The legend configuration
* @public
Expand Down Expand Up @@ -418,6 +447,10 @@ export interface LegendSpec {
* A SeriesSortFn to sort the legend values (top-bottom)
*/
legendSort?: SeriesCompareFn;
/**
* Override the legend with a custom component.
*/
customLegend?: CustomLegend;
}

/**
Expand Down
17 changes: 3 additions & 14 deletions packages/charts/src/specs/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { SeriesIdentifier } from '../common/series_id';
import { TooltipPortalSettings } from '../components/portal';
import { CustomTooltip } from '../components/tooltip';
import { buildSFProps, SFProps, useSpecFactory } from '../state/spec_factory';
import { Accessor } from '../utils/accessor';
import { PointerValue } from '../state/types';
import { Datum, stripUndefined } from '../utils/common';
import { SpecType, TooltipStickTo, TooltipType } from './constants';
import { Spec } from './index';
Expand All @@ -25,19 +25,12 @@ import { SettingsSpec } from './settings';
* This interface describe the properties of single value shown in the tooltip
* @public
*/
export interface TooltipValue<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier> {
export interface TooltipValue<D extends BaseDatum = Datum, SI extends SeriesIdentifier = SeriesIdentifier>
extends PointerValue<D> {
/**
* The label of the tooltip value
*/
label: string;
/**
* The value
*/
value: any;
/**
* The formatted value to display
*/
formattedValue: string;
/**
* The mark value
*/
Expand All @@ -62,10 +55,6 @@ export interface TooltipValue<D extends BaseDatum = Datum, SI extends SeriesIden
* The identifier of the related series
*/
seriesIdentifier: SI;
/**
* The accessor linked to the current tooltip value
*/
valueAccessor?: Accessor<D>;
/**
* The datum associated with the current tooltip value
* Maybe not available
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const getLegendConfigSelector = createCustomCachedSelector(
legendPosition,
legendStrategy,
onLegendItemClick,
customLegend,
showLegend,
onLegendItemMinusClick,
onLegendItemOut,
Expand All @@ -38,6 +39,7 @@ export const getLegendConfigSelector = createCustomCachedSelector(
legendPosition: getLegendPositionConfig(legendPosition),
legendStrategy,
onLegendItemClick,
customLegend,
showLegend,
onLegendItemMinusClick,
onLegendItemOut,
Expand Down
21 changes: 21 additions & 0 deletions packages/charts/src/state/selectors/get_pointer_value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { GlobalChartState } from '../chart_state';
import { PointerValue } from '../types';

/** @internal */
export const getPointerValueSelector = (state: GlobalChartState): PointerValue | undefined => {
// TODO: this is taken from the tooltip header currently. Should in the future
// be implemented separately (and probably used *as* the tooltip header).
const header = state.internalChartState?.getTooltipInfo(state)?.header;
if (header) {
const { value, formattedValue, valueAccessor } = header;
return { value, formattedValue, valueAccessor };
}
};
25 changes: 23 additions & 2 deletions packages/charts/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types';
import { Pixels } from '../common/geometry';
import { AnnotationType, LineAnnotationDatum, RectAnnotationDatum } from '../specs';
import type { Position } from '../utils/common';
import { AnnotationType, BaseDatum, LineAnnotationDatum, RectAnnotationDatum } from '../specs';
import { Accessor } from '../utils/accessor';
import type { Datum, Position } from '../utils/common';
import type { GeometryValue } from '../utils/geometry';
import { LineAnnotationStyle, RectAnnotationStyle } from '../utils/themes/theme';

Expand Down Expand Up @@ -132,3 +133,23 @@ export interface DebugState {
heatmap?: HeatmapDebugState;
partition?: PartitionDebugState[];
}

/**
* Contains the value of the non-dependent variable at the point where the mouse
* pointer is.
* @public
*/
export interface PointerValue<D extends BaseDatum = Datum> {
/**
* The value
*/
value: any;
/**
* The formatted value to display
*/
formattedValue: string;
/**
* The accessor linked to the current tooltip value
*/
valueAccessor?: Accessor<D>;
}
Loading

0 comments on commit 2e1648d

Please sign in to comment.