Skip to content

Commit

Permalink
feat(Legend): add legend stats
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed May 15, 2024
1 parent b1c72df commit 61da4e8
Show file tree
Hide file tree
Showing 35 changed files with 1,144 additions and 354 deletions.
5 changes: 3 additions & 2 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,7 @@ export type LegendItemListener = (series: SeriesIdentifier[]) => void;
export type LegendItemValue = {
value: PrimitiveValue;
label: string;
type?: LegendValue;
};

// @public (undocumented)
Expand Down Expand Up @@ -1854,6 +1855,7 @@ export interface LegendSpec {
legendSize: number;
legendSort?: SeriesCompareFn;
legendStrategy?: LegendStrategy;
legendTitle?: string;
legendValues: Array<LegendValue>;
// (undocumented)
onLegendItemClick?: LegendItemListener;
Expand Down Expand Up @@ -1892,7 +1894,6 @@ export interface LegendStyle {

// @public (undocumented)
export const LegendValue: Readonly<{
None: "none";
CurrentAndLastValue: "currentAndLastValue";
LastValue: "lastValue";
LastNonNullValue: "lastNonNullValue";
Expand Down Expand Up @@ -2738,7 +2739,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", "debug" | "locale" | "rotation" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "dow" | "showLegend" | "legendPosition" | "legendValues" | "legendMaxDepth" | "legendSize" | "flatLegend", "ariaDescription" | "ariaLabel" | "xDomain" | "theme" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onResize" | "onRenderChange" | "onWillRender" | "onProjectionAreaChange" | "onAnnotationClick" | "resizeDebounce" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "orderOrdinalBinsBy" | "noResults" | "ariaLabelledBy" | "ariaDescribedBy" | "ariaTableCaption" | "legendStrategy" | "onLegendItemOver" | "onLegendItemOut" | "onLegendItemClick" | "onLegendItemPlusClick" | "onLegendItemMinusClick" | "legendAction" | "legendColorPicker" | "legendSort" | "customLegend", never>;
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "debug" | "locale" | "rotation" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "dow" | "showLegend" | "legendPosition" | "legendValues" | "legendMaxDepth" | "legendSize" | "flatLegend", "ariaDescription" | "ariaLabel" | "xDomain" | "theme" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onResize" | "onRenderChange" | "onWillRender" | "onProjectionAreaChange" | "onAnnotationClick" | "resizeDebounce" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "orderOrdinalBinsBy" | "noResults" | "ariaLabelledBy" | "ariaDescribedBy" | "ariaTableCaption" | "legendStrategy" | "onLegendItemOver" | "onLegendItemOut" | "onLegendItemClick" | "onLegendItemPlusClick" | "onLegendItemMinusClick" | "legendAction" | "legendColorPicker" | "legendSort" | "customLegend" | "legendTitle", never>;

// @public (undocumented)
export type SettingsProps = ComponentProps<typeof Settings>;
Expand Down
39 changes: 13 additions & 26 deletions packages/charts/src/chart_types/xy_chart/legend/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { Color } from '../../../common/colors';
import { LegendItem, LegendValue } from '../../../common/legend';
import { LegendItem } from '../../../common/legend';
import { SeriesKey, SeriesIdentifier } from '../../../common/series_id';
import { SettingsSpec } from '../../../specs';
import { isDefined, mergePartial } from '../../../utils/common';
Expand All @@ -16,7 +16,7 @@ import { getLegendCompareFn, SeriesCompareFn } from '../../../utils/series_sort'
import { PointStyle, Theme } from '../../../utils/themes/theme';
import { XDomain } from '../domains/types';
import { isDatumFilled } from '../rendering/utils';
import { getLegendValue } from '../state/utils/get_last_value';
import { getLegendValues } from '../state/utils/get_legend_values';
import { getAxesSpecForSpecId, getSpecsById } from '../state/utils/spec';
import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip';
import { defaultTickFormatter } from '../utils/axis_utils';
Expand Down Expand Up @@ -109,7 +109,7 @@ export function computeLegend(
const legendItems: LegendItem[] = [];
const defaultColor = theme.colors.defaultVizColor;

const legendValueMode = settingsSpec.legendValues[0] ?? LegendValue.None;
const legendValues = settingsSpec.legendValues ?? [];

dataSeries.forEach((series) => {
const { specId, yAccessor } = series;
Expand Down Expand Up @@ -140,8 +140,7 @@ export function computeLegend(

const pointStyle = getPointStyle(spec, theme);

const itemValue = getLegendValue(series, xDomain, legendValueMode, y1Accessor(series.stackMode));
const formattedItemValue = itemValue !== null ? formatter(itemValue) : '';
const legendValuesItems = getLegendValues(series, xDomain, legendValues, y1Accessor(series.stackMode), formatter);

legendItems.push({
depth: 0,
Expand All @@ -152,22 +151,19 @@ export function computeLegend(
isSeriesHidden,
isItemHidden: hideInLegend,
isToggleable: true,
values:
itemValue !== null
? [
{
value: itemValue,
label: formattedItemValue,
},
]
: [],
values: legendValuesItems,
path: [{ index: 0, value: seriesIdentifier.key }],
keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()],
pointStyle,
});
if (banded) {
const bandedItemValue = getLegendValue(series, xDomain, legendValueMode, y0Accessor(series.stackMode));
const bandedFormattedItemValue = bandedItemValue !== null ? formatter(bandedItemValue) : '';
const bandedLegendValuesItems = getLegendValues(
series,
xDomain,
legendValues,
y0Accessor(series.stackMode),
formatter,
);

const labelY0 = getBandedLegendItemLabel(name, BandedAccessorType.Y0, postFixes);
legendItems.push({
Expand All @@ -179,15 +175,7 @@ export function computeLegend(
isSeriesHidden,
isItemHidden: hideInLegend,
isToggleable: true,
values:
bandedItemValue !== null
? [
{
value: bandedItemValue,
label: bandedFormattedItemValue,
},
]
: [],
values: bandedLegendValuesItems,
path: [{ index: 0, value: seriesIdentifier.key }],
keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()],
pointStyle,
Expand All @@ -201,7 +189,6 @@ export function computeLegend(
return defaultXYLegendSeriesSort(aDs, bDs);
});
const sortFn: SeriesCompareFn = settingsSpec.legendSort ?? legendSortFn;

return groupBy(
legendItems.sort((a, b) =>
a.seriesIdentifiers[0] && b.seriesIdentifiers[0] ? sortFn(a.seriesIdentifiers[0], b.seriesIdentifiers[0]) : 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,58 @@ import { LegendValue } from '../../../../common/legend';
import { ScaleType } from '../../../../scales/constants';
import { XDomain } from '../../domains/types';
import { DataSeries, DataSeriesDatum } from '../../utils/series';
import { TickFormatter } from '../../utils/specs';

/** @internal */
export function nonNullable<T>(v: T): v is NonNullable<T> {
return v !== null || v !== undefined;
}

/** @internal */
export const legendValueTitlesMap = {
[LegendValue.CurrentAndLastValue]: 'Value',
[LegendValue.Value]: 'Value',
[LegendValue.Percent]: 'Percent',
[LegendValue.LastValue]: 'Last',
[LegendValue.LastNonNullValue]: 'Last non-null',
[LegendValue.FirstValue]: 'First',
[LegendValue.FirstNonNullValue]: 'First non-null',
[LegendValue.Average]: 'Avg',
[LegendValue.Median]: 'Mid',
[LegendValue.Min]: 'Min',
[LegendValue.Max]: 'Max',
[LegendValue.Total]: 'Total',
[LegendValue.Count]: 'Count',
[LegendValue.DistinctCount]: 'Dist Count',
[LegendValue.Variance]: 'Variance',
[LegendValue.StdDeviation]: 'Std dev',
[LegendValue.Range]: 'Range',
[LegendValue.Difference]: 'Diff',
[LegendValue.DifferencePercent]: 'Diff %',
};

/**
* This method return legend values from a DataSeries that correspond to the type of value requested.
* It in general compute the last, min, max, avg, sum of the value in a series.
* @internal
*/
export function getLegendValues(
series: DataSeries,
xDomain: XDomain,
types: LegendValue[],
valueAccessor: (d: DataSeriesDatum) => number | null,
formatter: TickFormatter<any> | ((tick: unknown) => string),
) {
return types.map((type) => {
const value = getLegendValue(series, xDomain, type, valueAccessor) ?? null;

return {
type,
label: typeof value === 'number' ? formatter(value) : '',
value,
};
});
}

/**
* This method return a value from a DataSeries that correspond to the type of value requested.
Expand Down Expand Up @@ -80,7 +132,6 @@ export function getLegendValue(
case LegendValue.DifferencePercent:
return differencePercent(series.data, valueAccessor);
default:
case LegendValue.None:
return null;
}
}
5 changes: 2 additions & 3 deletions packages/charts/src/common/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import { PointStyle } from '../utils/themes/theme';
export type LegendItemChildId = CategoryKey;

/** @public */
export type LegendItemValue = { value: PrimitiveValue; label: string };
export type LegendItemValue = { value: PrimitiveValue; label: string; type?: LegendValue };

/** @public */
export const LegendValue = Object.freeze({
None: 'none' as const,
/** Value of the bucket being hovered or last bucket value when not hovering. */
CurrentAndLastValue: 'currentAndLastValue' as const,
/** Last value considering all data points in the chart */
Expand Down Expand Up @@ -81,7 +80,7 @@ export type LegendItem = {
label: CategoryLabel;
isSeriesHidden?: boolean;
isItemHidden?: boolean;
values: Array<LegendItemValue>;
values: LegendItemValue[];
// TODO: Remove when partition layers are toggleable
isToggleable?: boolean;
keys: Array<string | number>;
Expand Down
28 changes: 15 additions & 13 deletions packages/charts/src/components/__snapshots__/chart.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@ exports[`Chart should render the legend name test 1`] = `
<div className="echLegend echLegend--debug echLegend--vertical echLegend--right echLegend--top" style={{...}} dir="ltr">
<div style={{...}} className="echLegendListContainer">
<ul style={{...}} className="echLegendList">
<LegendItem item={{...}} positionConfig={{...}} isMostlyRTL={false} totalItems={1} extraValues={{...}} legendValues={{...}} onMouseOut={[undefined]} onMouseOver={[undefined]} onClick={[undefined]} clearTemporaryColorsAction={[Function (anonymous)]} setPersistedColorAction={[Function (anonymous)]} setTemporaryColorAction={[Function (anonymous)]} mouseOutAction={[Function (anonymous)]} mouseOverAction={[Function (anonymous)]} toggleDeselectSeriesAction={[Function (anonymous)]} colorPicker={[undefined]} action={[undefined]} labelOptions={{...}} flatLegend={false}>
<LegendItem item={{...}} positionConfig={{...}} isMostlyRTL={false} totalItems={1} extraValues={{...}} legendValues={{...}} onMouseOut={[undefined]} onMouseOver={[undefined]} onClick={[undefined]} clearTemporaryColorsAction={[Function (anonymous)]} setPersistedColorAction={[Function (anonymous)]} setTemporaryColorAction={[Function (anonymous)]} mouseOutAction={[Function (anonymous)]} mouseOverAction={[Function (anonymous)]} toggleDeselectSeriesAction={[Function (anonymous)]} colorPicker={[undefined]} action={[undefined]} labelOptions={{...}} flatLegend={false} legendTitle={[undefined]}>
<li className="echLegendItem echLegendItem--vertical" onMouseEnter={[Function (anonymous)]} onMouseLeave={[Function (anonymous)]} style={{...}} dir="ltr" data-ech-series-name="test">
<div className="background" />
<div className="colorWrapper">
<ForwardRef color="#54B399" seriesName="test" isSeriesHidden={false} hasColorPicker={false} onClick={[undefined]} pointStyle={[undefined]}>
<div className="echLegendItem__color" title="series color">
<LegendIcon pointStyle={[undefined]} color="#54B399" ariaLabel="series color: #54B399">
<svg width={16} height={16} aria-label="series color: #54B399">
<g transform="\\n translate(8, 8)\\n rotate(0)\\n ">
<path d="M -3.5 0 a 3.5,3.5 0 1,0 7,0 a 3.5,3.5 0 1,0 -7,0" stroke="#54B399" strokeWidth={1} fill="#54B399" opacity={1} />
</g>
</svg>
</LegendIcon>
</div>
</ForwardRef>
<div className="echLegend__colorWrapper">
<LegendColorPicker item={{...}} positionConfig={{...}} isMostlyRTL={false} totalItems={1} extraValues={{...}} legendValues={{...}} onMouseOut={[undefined]} onMouseOver={[undefined]} onClick={[undefined]} clearTemporaryColorsAction={[Function (anonymous)]} setPersistedColorAction={[Function (anonymous)]} setTemporaryColorAction={[Function (anonymous)]} mouseOutAction={[Function (anonymous)]} mouseOverAction={[Function (anonymous)]} toggleDeselectSeriesAction={[Function (anonymous)]} colorPicker={[undefined]} action={[undefined]} labelOptions={{...}} flatLegend={false} legendTitle={[undefined]}>
<ForwardRef color="#54B399" seriesName="test" isSeriesHidden={false} hasColorPicker={false} onClick={[undefined]} pointStyle={[undefined]}>
<div className="echLegendItem__color" title="series color">
<LegendIcon pointStyle={[undefined]} color="#54B399" ariaLabel="series color: #54B399">
<svg width={16} height={16} aria-label="series color: #54B399">
<g transform="\\n translate(8, 8)\\n rotate(0)\\n ">
<path d="M -3.5 0 a 3.5,3.5 0 1,0 7,0 a 3.5,3.5 0 1,0 -7,0" stroke="#54B399" strokeWidth={1} fill="#54B399" opacity={1} />
</g>
</svg>
</LegendIcon>
</div>
</ForwardRef>
</LegendColorPicker>
</div>
<Label label="test" options={{...}} isToggleable={false} onToggle={[undefined]} isSeriesHidden={false}>
<div dir="ltr" className="echLegendItem__label echLegendItem__label--singleline" title="test" style={{...}}>
Expand Down
1 change: 1 addition & 0 deletions packages/charts/src/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import 'container';
@import 'brush/index';
@import 'tooltip/components/index';
@import 'legend/components/index';
@import 'portal/index';
@import 'icons/index';
@import 'legend/index';
Expand Down
Loading

0 comments on commit 61da4e8

Please sign in to comment.