Skip to content

Commit

Permalink
feat(legend): display pie chart legend extra (opensearch-project#939)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahzad31 authored Jan 20, 2021
1 parent ae0d1dc commit 672a4df
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 8 deletions.
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.
21 changes: 21 additions & 0 deletions packages/osd-charts/integration/tests/legend_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { PartitionLayout } from '../../src';
import { common } from '../page_objects';

describe('Legend stories', () => {
Expand Down Expand Up @@ -143,4 +144,24 @@ describe('Legend stories', () => {
expect(hiddenResults).toEqual([1]);
});
});

describe('Extra values', () => {
it.each([PartitionLayout.sunburst, PartitionLayout.treemap])(
'should display flat legend extra values on %s',
async (layout) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/legend--piechart&knob-Partition Layout=${layout}&knob-flatLegend=true&knob-showLegendExtra=true&knob-legendMaxDepth=2`,
);
},
);

it.each([PartitionLayout.sunburst, PartitionLayout.treemap])(
'should display nested legend extra values on %s',
async (layout) => {
await common.expectChartAtUrlToMatchScreenshot(
`http://localhost:9001/?path=/story/legend--piechart&knob-Partition Layout=${layout}&knob-flatLegend=false&knob-showLegendExtra=true&knob-legendMaxDepth=2`,
);
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Partition } from '../renderer/canvas/partition';
import { HighlighterFromHover } from '../renderer/dom/highlighter_hover';
import { HighlighterFromLegend } from '../renderer/dom/highlighter_legend';
import { computeLegendSelector } from './selectors/compute_legend';
import { getLegendItemsExtra } from './selectors/get_legend_items_extra';
import { getLegendItemsLabels } from './selectors/get_legend_items_labels';
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
import { createOnElementClickCaller } from './selectors/on_element_click_caller';
Expand All @@ -37,8 +38,6 @@ import { createOnElementOverCaller } from './selectors/on_element_over_caller';
import { getPieSpec } from './selectors/pie_spec';
import { getTooltipInfoSelector } from './selectors/tooltip';

const EMPTY_MAP = new Map();

/** @internal */
export class PartitionState implements InternalChartState {
chartType = ChartTypes.Partition;
Expand Down Expand Up @@ -80,8 +79,8 @@ export class PartitionState implements InternalChartState {
return computeLegendSelector(globalState);
}

getLegendExtraValues() {
return EMPTY_MAP;
getLegendExtraValues(globalState: GlobalChartState) {
return getLegendItemsExtra(globalState);
}

chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject<HTMLCanvasElement>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Partition - Legend item extra values should return all extra values in nested legend 1`] = `Object {}`;

exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 1 1`] = `Object {}`;

exports[`Partition - Legend item extra values should return extra values in nested legend within max depth of 2 1`] = `Object {}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Store } from 'redux';

import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs';
import { MockStore } from '../../../../mocks/store';
import { GlobalChartState } from '../../../../state/chart_state';
import { PrimitiveValue } from '../../layout/utils/group_by_rollup';
import { getLegendItemsExtra } from './get_legend_items_extra';

describe('Partition - Legend item extra values', () => {
type TestDatum = [string, string, string, number];
const spec = MockSeriesSpec.sunburst({
data: [
['aaa', 'aa', '1', 1],
['aaa', 'aa', '1', 2],
['aaa', 'aa', '3', 1],
['aaa', 'bb', '4', 1],
['aaa', 'bb', '5', 1],
['aaa', 'bb', '6', 1],
['bbb', 'aa', '7', 1],
['bbb', 'aa', '8', 1],
['bbb', 'bb', '9', 1],
['bbb', 'bb', '10', 1],
['bbb', 'cc', '11', 1],
['bbb', 'cc', '12', 1],
],
valueAccessor: (d: TestDatum) => d[3],
layers: [
{
groupByRollup: (datum: TestDatum) => datum[0],
nodeLabel: (d: PrimitiveValue) => String(d),
},
{
groupByRollup: (datum: TestDatum) => datum[1],
nodeLabel: (d: PrimitiveValue) => String(d),
},
{
groupByRollup: (datum: TestDatum) => datum[2],
nodeLabel: (d: PrimitiveValue) => String(d),
},
],
});
let store: Store<GlobalChartState>;

beforeEach(() => {
store = MockStore.default();
});

it('should return all extra values in nested legend', () => {
MockStore.addSpecs([spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([
'0',
'0__0',
'0__0__0',
'0__0__0__0',
'0__0__0__1',
'0__0__1',
'0__0__1__0',
'0__0__1__1',
'0__0__1__2',
'0__1',
'0__1__0',
'0__1__0__0',
'0__1__0__1',
'0__1__1',
'0__1__1__0',
'0__1__1__1',
'0__1__2',
'0__1__2__0',
'0__1__2__1',
]);
expect(extraValues.values()).toMatchSnapshot();
});

it('should return extra values in nested legend within max depth of 1', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 1 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual(['0', '0__0', '0__1']);
expect(extraValues.values()).toMatchSnapshot();
});

it('should return extra values in nested legend within max depth of 2', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 2 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([
'0',
'0__0',
'0__0__0',
'0__0__1',
'0__1',
'0__1__0',
'0__1__1',
'0__1__2',
]);
expect(extraValues.values()).toMatchSnapshot();
});

it('filters all extraValues is depth is 0', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: 0 });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([]);
});

it('filters all extraValues is depth is NaN', () => {
const settings = MockGlobalSpec.settings({ legendMaxDepth: NaN });
MockStore.addSpecs([settings, spec], store);

const extraValues = getLegendItemsExtra(store.getState());
expect([...extraValues.keys()]).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import createCachedSelector from 're-reselect';

import { LegendItemExtraValues } from '../../../../common/legend';
import { SeriesKey } from '../../../../common/series_id';
import { SettingsSpec } from '../../../../specs';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { HierarchyOfArrays, CHILDREN_KEY } from '../../layout/utils/group_by_rollup';
import { PartitionSpec } from '../../specs';
import { getPieSpec } from './pie_spec';
import { getTree } from './tree';

/** @internal */
export const getLegendItemsExtra = createCachedSelector(
[getPieSpec, getSettingsSpecSelector, getTree],
(pieSpec, { legendMaxDepth }, tree): Map<SeriesKey, LegendItemExtraValues> => {
const legendExtraValues = new Map<SeriesKey, LegendItemExtraValues>();

return pieSpec && isValidLegendMaxDepth(legendMaxDepth)
? getExtraValueMap(pieSpec, tree, legendMaxDepth)
: legendExtraValues;
},
)(getChartIdSelector);

/**
* Check if the legendMaxDepth from settings is a valid number (NaN or <=0)
*
* @param legendMaxDepth - SettingsSpec['legendMaxDepth']
*/
function isValidLegendMaxDepth(legendMaxDepth: SettingsSpec['legendMaxDepth']): boolean {
return typeof legendMaxDepth === 'number' && !Number.isNaN(legendMaxDepth) && legendMaxDepth > 0;
}

/**
* Creates flat extra value map from nested key path
*/
function getExtraValueMap(
{ layers, valueFormatter }: Pick<PartitionSpec, 'layers' | 'valueFormatter'>,
tree: HierarchyOfArrays,
maxDepth: number,
depth: number = 0,
keys: Map<SeriesKey, LegendItemExtraValues> = new Map(),
): Map<SeriesKey, LegendItemExtraValues> {
for (let i = 0; i < tree.length; i++) {
const branch = tree[i];
const [key, arrayNode] = branch;
const { value, path, [CHILDREN_KEY]: children } = arrayNode;

if (key != null) {
const values: LegendItemExtraValues = new Map();
const formattedValue = valueFormatter ? valueFormatter(value) : value;

values.set(key, formattedValue);
keys.set(path.map(({ index }) => index).join('__'), values);
}

if (depth < maxDepth) {
getExtraValueMap({ layers, valueFormatter }, children, maxDepth, depth + 1, keys);
}
}
return keys;
}
4 changes: 4 additions & 0 deletions packages/osd-charts/src/common/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LegendPath } from '../state/actions/legend';
import { Color } from '../utils/common';
import { CategoryKey, CategoryLabel } from './category';
import { SeriesIdentifier } from './series_id';

/** @internal */
export type LegendItemChildId = CategoryKey;

Expand All @@ -30,6 +31,9 @@ export type LegendItem = {
seriesIdentifier: SeriesIdentifier;
childId?: LegendItemChildId;
depth?: number;
/**
* Path to iterm in hierarchical legend
*/
path: LegendPath;
color: Color;
label: CategoryLabel;
Expand Down
4 changes: 2 additions & 2 deletions packages/osd-charts/src/components/legend/legend_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
'echLegendItem__extra--hidden': isItemHidden,
});
const hasColorPicker = Boolean(colorPicker);
const extra = getExtra(extraValues, item, totalItems);
const extra = showExtra && getExtra(extraValues, item, totalItems);
const style = item.depth
? {
marginLeft: LEGEND_HIERARCHY_MARGIN * (item.depth ?? 0),
Expand Down Expand Up @@ -241,7 +241,7 @@ export class LegendListItem extends Component<LegendItemProps, LegendItemState>
onClick={this.handleLabelClick(seriesIdentifier)}
isSeriesHidden={isSeriesHidden}
/>
{showExtra && extra && renderExtra(extra, isSeriesHidden)}
{extra && renderExtra(extra, isSeriesHidden)}
{Action && (
<div className="echLegendItem__action">
<Action series={seriesIdentifier} color={color} label={label} />
Expand Down
4 changes: 3 additions & 1 deletion packages/osd-charts/src/components/legend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export function getExtra(extraValues: Map<string, LegendItemExtraValues>, item:
seriesIdentifier: { key },
defaultExtra,
childId,
path,
} = item;
if (extraValues.size === 0) {
return defaultExtra?.formatted ?? '';
}
const itemExtraValues = extraValues.get(key);
const extraValueKey = path.map(({ index }) => index).join('__');
const itemExtraValues = extraValues.has(extraValueKey) ? extraValues.get(extraValueKey) : extraValues.get(key);
const actionExtra = (childId && itemExtraValues?.get(childId)) ?? null;
if (extraValues.size !== totalItems) {
if (actionExtra != null) {
Expand Down
12 changes: 11 additions & 1 deletion packages/osd-charts/stories/legend/10_sunburst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ import {
} from '../utils/utils';

export const Example = () => {
const partitionLayout = select(
'Partition Layout',
{
treemap: PartitionLayout.treemap,
sunburst: PartitionLayout.sunburst,
},
PartitionLayout.sunburst,
);
const flatLegend = boolean('flatLegend', true);
const showLegendExtra = boolean('showLegendExtra', false);
const legendMaxDepth = number('legendMaxDepth', 2, {
min: 0,
max: 3,
Expand All @@ -46,6 +55,7 @@ export const Example = () => {
<Chart className="story-chart">
<Settings
showLegend
showLegendExtra={showLegendExtra}
flatLegend={flatLegend}
legendStrategy={legendStrategy}
legendMaxDepth={legendMaxDepth}
Expand Down Expand Up @@ -81,7 +91,7 @@ export const Example = () => {
},
]}
config={{
partitionLayout: PartitionLayout.sunburst,
partitionLayout,
linkLabel: {
maxCount: 0,
fontSize: 14,
Expand Down

0 comments on commit 672a4df

Please sign in to comment.