Skip to content

Commit

Permalink
Heatmap small multiples (#154434)
Browse files Browse the repository at this point in the history
## Summary

Enables split chart in agg based heatmaps (small multiples) and makes
the EC implementation the default. It also marks the advanced setting
that changes the implementation as deprecated.

<img width="2493" alt="image"
src="https://user-images.githubusercontent.com/17003240/230048435-f8b20240-5005-4343-ba18-e2c7307945e0.png">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marco Vettorello <[email protected]>
Co-authored-by: Nick Partridge <[email protected]>
Co-authored-by: nickofthyme <[email protected]>
  • Loading branch information
5 people authored Apr 20, 2023
1 parent 40d730b commit 544f908
Show file tree
Hide file tree
Showing 20 changed files with 338 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { Accessor, AccessorFn, GroupBy, SmallMultiples, Predicate } from '@elastic/charts';

interface ChartSplitProps {
splitColumnAccessor?: Accessor | AccessorFn;
splitRowAccessor?: Accessor | AccessorFn;
}

const CHART_SPLIT_ID = '__heatmap_chart_split__';
const SMALL_MULTIPLES_ID = '__heatmap_chart_sm__';

export const ChartSplit = ({ splitColumnAccessor, splitRowAccessor }: ChartSplitProps) => {
if (!splitColumnAccessor && !splitRowAccessor) return null;

return (
<>
<GroupBy
id={CHART_SPLIT_ID}
by={(spec, datum) => {
const splitTypeAccessor = splitColumnAccessor || splitRowAccessor;
if (splitTypeAccessor) {
return typeof splitTypeAccessor === 'function'
? splitTypeAccessor(datum)
: datum[splitTypeAccessor];
}
return spec.id;
}}
sort={Predicate.DataIndex}
/>
<SmallMultiples
id={SMALL_MULTIPLES_ID}
splitVertically={splitRowAccessor ? CHART_SPLIT_ID : undefined}
splitHorizontally={splitColumnAccessor ? CHART_SPLIT_ID : undefined}
style={{
verticalPanelPadding: {
outer: 0,
inner: 0.1,
},
horizontalPanelPadding: {
outer: 0,
inner: 0.1,
},
}}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
LegendColorPickerWrapper,
} from '../utils/get_color_picker';
import { defaultPaletteParams } from '../constants';
import { ChartSplit } from './chart_split';
import { getSplitDimensionAccessor, createSplitPoint } from '../utils/get_split_dimension_utils';
import './index.scss';

declare global {
Expand Down Expand Up @@ -207,14 +209,19 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
() => findMinMaxByColumnId([valueAccessor!], table),
[valueAccessor, table]
);

const paletteParams = args.palette?.params;
const xAccessor = args.xAccessor
? getAccessorByDimension(args.xAccessor, table.columns)
: undefined;
const yAccessor = args.yAccessor
? getAccessorByDimension(args.yAccessor, table.columns)
: undefined;
const splitChartRowAccessor = args.splitRowAccessor
? getSplitDimensionAccessor(data.columns, args.splitRowAccessor, formatFactory)
: undefined;
const splitChartColumnAccessor = args.splitColumnAccessor
? getSplitDimensionAccessor(data.columns, args.splitColumnAccessor, formatFactory)
: undefined;

const xAxisColumnIndex = table.columns.findIndex((v) => v.id === xAccessor);
const yAxisColumnIndex = table.columns.findIndex((v) => v.id === yAccessor);
Expand Down Expand Up @@ -252,7 +259,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
const onElementClick = useCallback(
(e: HeatmapElementEvent[]) => {
const cell = e[0][0];
const { x, y } = cell.datum;
const { x, y, smVerticalAccessorValue, smHorizontalAccessorValue } = cell.datum;

const points = [
{
Expand Down Expand Up @@ -284,6 +291,28 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
: []),
];

if (smHorizontalAccessorValue && args.splitColumnAccessor) {
const point = createSplitPoint(
args.splitColumnAccessor,
smHorizontalAccessorValue,
formatFactory,
table
);
if (point) {
points.push(point);
}
}
if (smVerticalAccessorValue && args.splitRowAccessor) {
const point = createSplitPoint(
args.splitRowAccessor,
smVerticalAccessorValue,
formatFactory,
table
);
if (point) {
points.push(point);
}
}
const context: FilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
Expand All @@ -295,6 +324,9 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
onClickValue(context);
},
[
args.splitColumnAccessor,
args.splitRowAccessor,
formatFactory,
formattedTable.formattedColumns,
onClickValue,
table,
Expand Down Expand Up @@ -579,6 +611,10 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
}}
>
<Chart ref={chartRef}>
<ChartSplit
splitColumnAccessor={splitChartColumnAccessor}
splitRowAccessor={splitChartRowAccessor}
/>
<Settings
onRenderChange={onRenderChange}
noResults={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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 { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import type { DatatableColumn, Datatable } from '@kbn/expressions-plugin/public';
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { getSplitDimensionAccessor, createSplitPoint } from './get_split_dimension_utils';

const data: Datatable = {
type: 'datatable',
rows: [
{ 'col-0-1': 0, 'col-1-2': 'a', 'col-2-3': 'd' },
{ 'col-0-1': 148, 'col-1-2': 'b', 'col-2-3': 'c' },
],
columns: [
{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } },
{ id: 'col-1-2', name: 'Dest', meta: { type: 'string' } },
{
id: 'col-2-3',
name: 'Test',
meta: {
type: 'number',
params: {
id: 'number',
},
},
},
],
};

describe('getSplitDimensionAccessor', () => {
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));

beforeEach(() => {
defaultFormatter.mockClear();
});

const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: {
id: data.columns[2].id,
name: data.columns[2].name,
meta: data.columns[2].meta,
},
format: {
params: {},
},
};

it('returns accessor which is using formatter, if meta.params are present at accessing column', () => {
const accessor = getSplitDimensionAccessor(data.columns, splitDimension, defaultFormatter);

expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(typeof accessor).toBe('function');
accessor(data.rows[0]);
});

it('returns accessor which is using default formatter, if meta.params and format are not present', () => {
const column: Partial<DatatableColumn> = {
...data.columns[2],
meta: { type: 'number' },
};
const columns = [data.columns[0], column, data.columns[2]] as DatatableColumn[];
const defaultFormatterReturnedVal = fieldFormatsMock.deserialize();
const spyOnDefaultFormatterConvert = jest.spyOn(defaultFormatterReturnedVal, 'convert');

defaultFormatter.mockReturnValueOnce(defaultFormatterReturnedVal);
const accessor = getSplitDimensionAccessor(columns, splitDimension, defaultFormatter);

expect(defaultFormatter).toHaveBeenCalledTimes(1);

expect(typeof accessor).toBe('function');
accessor(data.rows[0]);
expect(spyOnDefaultFormatterConvert).toHaveBeenCalledTimes(1);
});

it('returns accessor which returns undefined, if such column is not present', () => {
const accessor1 = getSplitDimensionAccessor(data.columns, splitDimension, defaultFormatter);

expect(typeof accessor1).toBe('function');
const result1 = accessor1({});
expect(result1).toBeUndefined();

const column2: Partial<DatatableColumn> = {
...data.columns[2],
meta: { type: 'string' },
};
const columns2 = [data.columns[0], data.columns[1], column2] as DatatableColumn[];
const accessor2 = getSplitDimensionAccessor(columns2, splitDimension, defaultFormatter);

expect(typeof accessor2).toBe('function');
const result2 = accessor1({});
expect(result2).toBeUndefined();

const column3 = {
...data.columns[2],
meta: { type: 'string' },
format: {
id: 'string',
params: {},
},
};
const columns3 = [data.columns[0], data.columns[1], column3] as DatatableColumn[];

const accessor3 = getSplitDimensionAccessor(columns3, splitDimension, defaultFormatter);
expect(typeof accessor3).toBe('function');
const result3 = accessor3({});
expect(result3).toBeUndefined();
});
});

describe('createSplitPoint', () => {
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));

beforeEach(() => {
defaultFormatter.mockClear();
});

const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: {
id: data.columns[2].id,
name: data.columns[2].name,
meta: data.columns[2].meta,
},
format: {
params: {},
},
};

it('returns point if value is found in the table', () => {
const point = createSplitPoint(splitDimension, 'c', defaultFormatter, data);

expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(point).toStrictEqual({ column: 2, row: 1, value: 'c' });
});

it('returns undefined if value is not found in the table', () => {
const point = createSplitPoint(splitDimension, 'test', defaultFormatter, data);

expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(point).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 type { AccessorFn } from '@elastic/charts';
import type { DatatableColumn, Datatable } from '@kbn/expressions-plugin/public';
import type { FormatFactory } from '@kbn/field-formats-plugin/common';
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';

export const getSplitDimensionAccessor = (
columns: DatatableColumn[],
splitDimension: ExpressionValueVisDimension | string,
formatFactory: FormatFactory
): AccessorFn => {
const splitChartColumn = getColumnByAccessor(splitDimension, columns)!;
const accessor = splitChartColumn.id;

const formatter = formatFactory(splitChartColumn.meta?.params);
const fn: AccessorFn = (d) => {
const v = d[accessor];
if (v === undefined) {
return;
}

const f = formatter.convert(v);
return f;
};

return fn;
};

export function createSplitPoint(
splitDimension: ExpressionValueVisDimension | string,
value: string | number,
formatFactory: FormatFactory,
table: Datatable
) {
const splitChartColumn = getColumnByAccessor(splitDimension, table.columns)!;
const accessor = splitChartColumn.id;

const formatter = formatFactory(splitChartColumn.meta?.params);
const splitPointRowIndex = table.rows.findIndex((row) => {
return formatter.convert(row[accessor]) === value;
});
if (splitPointRowIndex !== -1) {
return {
row: splitPointRowIndex,
column: table.columns.findIndex((column) => column.id === accessor),
value: table.rows[splitPointRowIndex][accessor],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts';

Expand All @@ -15,7 +14,6 @@ import { VIS_EVENT_TO_TRIGGER, VisTypeDefinition } from '@kbn/visualizations-plu
import { HeatmapTypeProps, HeatmapVisParams, AxisType, ScaleType } from '../types';
import { toExpressionAst } from '../to_ast';
import { getHeatmapOptions } from '../editor/components';
import { SplitTooltip } from './split_tooltip';
import { convertToLens } from '../convert_to_lens';

export const getHeatmapVisTypeDefinition = ({
Expand Down Expand Up @@ -129,11 +127,6 @@ export const getHeatmapVisTypeDefinition = ({
{
group: AggGroupNames.Buckets,
name: 'split',
// TODO: Remove when split chart aggs are supported
...(showElasticChartsOptions && {
disabled: true,
tooltip: <SplitTooltip />,
}),
title: i18n.translate('visTypeHeatmap.heatmap.splitTitle', {
defaultMessage: 'Split chart',
}),
Expand Down
19 changes: 0 additions & 19 deletions src/plugins/vis_types/heatmap/public/vis_type/split_tooltip.tsx

This file was deleted.

Loading

0 comments on commit 544f908

Please sign in to comment.