Skip to content

Commit d44144f

Browse files
authored
[ML] Data Grid Histograms (#68359) (#69607)
Adds support for histogram charts to data grid columns. - Adds a toggle button to the data grid's header to enabled/disable column charts. - When enabled, the charts get rendered as part of the data grid header. - Histogram charts will get rendered for fields based on date, number, string and boolean.
1 parent 8c27e76 commit d44144f

File tree

24 files changed

+996
-95
lines changed

24 files changed

+996
-95
lines changed

x-pack/plugins/ml/common/util/group_color_utils.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import euiVars from '@elastic/eui/dist/eui_theme_dark.json';
88

9+
import { stringHash } from './string_utils';
10+
911
const COLORS = [
1012
euiVars.euiColorVis0,
1113
euiVars.euiColorVis1,
@@ -33,17 +35,3 @@ export function tabColor(name: string): string {
3335
return colorMap[name];
3436
}
3537
}
36-
37-
function stringHash(str: string): number {
38-
let hash = 0;
39-
let chr = 0;
40-
if (str.length === 0) {
41-
return hash;
42-
}
43-
for (let i = 0; i < str.length; i++) {
44-
chr = str.charCodeAt(i);
45-
hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise
46-
hash |= 0; // eslint-disable-line no-bitwise
47-
}
48-
return hash < 0 ? hash * -2 : hash;
49-
}

x-pack/plugins/ml/common/util/string_utils.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { renderTemplate, getMedianStringLength } from './string_utils';
7+
import { renderTemplate, getMedianStringLength, stringHash } from './string_utils';
88

99
const strings: string[] = [
1010
'foo',
@@ -46,4 +46,12 @@ describe('ML - string utils', () => {
4646
expect(result).toBe(0);
4747
});
4848
});
49+
50+
describe('stringHash', () => {
51+
test('should return a unique number based off a string', () => {
52+
const hash1 = stringHash('the-string-1');
53+
const hash2 = stringHash('the-string-2');
54+
expect(hash1).not.toBe(hash2);
55+
});
56+
});
4957
});

x-pack/plugins/ml/common/util/string_utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,20 @@ export function getMedianStringLength(strings: string[]) {
2222
const sortedStringLengths = strings.map((s) => s.length).sort((a, b) => a - b);
2323
return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0;
2424
}
25+
26+
/**
27+
* Creates a deterministic number based hash out of a string.
28+
*/
29+
export function stringHash(str: string): number {
30+
let hash = 0;
31+
let chr = 0;
32+
if (str.length === 0) {
33+
return hash;
34+
}
35+
for (let i = 0; i < str.length; i++) {
36+
chr = str.charCodeAt(i);
37+
hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise
38+
hash |= 0; // eslint-disable-line no-bitwise
39+
}
40+
return hash < 0 ? hash * -2 : hash;
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.mlDataGridChart__histogram {
2+
width: 100%;
3+
height: $euiSizeXL + $euiSizeXXL;
4+
}
5+
6+
.mlDataGridChart__legend {
7+
@include euiTextTruncate;
8+
@include euiFontSizeXS;
9+
10+
color: $euiColorMediumShade;
11+
display: block;
12+
overflow-x: hidden;
13+
margin: $euiSizeXS 0px 0px 0px;
14+
font-style: italic;
15+
font-weight: normal;
16+
text-align: left;
17+
}
18+
19+
.mlDataGridChart__legend--numeric {
20+
text-align: right;
21+
}
22+
23+
.mlDataGridChart__legendBoolean {
24+
width: 100%;
25+
td { text-align: center }
26+
}
27+
28+
/* Override to align column header to bottom of cell when no chart is available */
29+
.mlDataGrid .euiDataGridHeaderCell__content {
30+
margin-top: auto;
31+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { FC } from 'react';
8+
import classNames from 'classnames';
9+
10+
import { BarSeries, Chart, Settings } from '@elastic/charts';
11+
import { EuiDataGridColumn } from '@elastic/eui';
12+
13+
import './column_chart.scss';
14+
15+
import { isUnsupportedChartData, useColumnChart, ChartData } from './use_column_chart';
16+
17+
interface Props {
18+
chartData: ChartData;
19+
columnType: EuiDataGridColumn;
20+
dataTestSubj: string;
21+
}
22+
23+
export const ColumnChart: FC<Props> = ({ chartData, columnType, dataTestSubj }) => {
24+
const { data, legendText, xScaleType } = useColumnChart(chartData, columnType);
25+
26+
return (
27+
<div data-test-subj={dataTestSubj}>
28+
{!isUnsupportedChartData(chartData) && data.length > 0 && (
29+
<div className="mlDataGridChart__histogram" data-test-subj={`${dataTestSubj}-histogram`}>
30+
<Chart>
31+
<Settings
32+
theme={{
33+
background: { color: 'transparent' },
34+
chartMargins: {
35+
left: 0,
36+
right: 0,
37+
top: 0,
38+
bottom: 1,
39+
},
40+
chartPaddings: {
41+
left: 0,
42+
right: 0,
43+
top: 0,
44+
bottom: 0,
45+
},
46+
scales: { barsPadding: 0.1 },
47+
}}
48+
/>
49+
<BarSeries
50+
id="histogram"
51+
name="count"
52+
xScaleType={xScaleType}
53+
yScaleType="linear"
54+
xAccessor="key"
55+
yAccessors={['doc_count']}
56+
styleAccessor={(d) => d.datum.color}
57+
data={data}
58+
/>
59+
</Chart>
60+
</div>
61+
)}
62+
<div
63+
className={classNames('mlDataGridChart__legend', {
64+
'mlDataGridChart__legend--numeric': columnType.schema === 'number',
65+
})}
66+
data-test-subj={`${dataTestSubj}-legend`}
67+
>
68+
{legendText}
69+
</div>
70+
<div data-test-subj={`${dataTestSubj}-id`}>{columnType.id}</div>
71+
</div>
72+
);
73+
};

x-pack/plugins/ml/public/application/components/data_grid/common.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ import {
1313
EuiDataGridStyle,
1414
} from '@elastic/eui';
1515

16+
import { i18n } from '@kbn/i18n';
17+
18+
import { CoreSetup } from 'src/core/public';
19+
1620
import {
1721
IndexPattern,
1822
IFieldType,
1923
ES_FIELD_TYPES,
2024
KBN_FIELD_TYPES,
2125
} from '../../../../../../../src/plugins/data/public';
2226

27+
import { extractErrorMessage } from '../../../../common/util/errors';
28+
2329
import {
2430
BASIC_NUMERICAL_TYPES,
2531
EXTENDED_NUMERICAL_TYPES,
@@ -37,7 +43,7 @@ import { mlFieldFormatService } from '../../services/field_format_service';
3743

3844
import { DataGridItem, IndexPagination, RenderCellValue } from './types';
3945

40-
export const INIT_MAX_COLUMNS = 20;
46+
export const INIT_MAX_COLUMNS = 10;
4147

4248
export const euiDataGridStyle: EuiDataGridStyle = {
4349
border: 'all',
@@ -102,6 +108,8 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
102108
case 'boolean':
103109
schema = 'boolean';
104110
break;
111+
case 'text':
112+
schema = NON_AGGREGATABLE;
105113
}
106114

107115
if (
@@ -122,7 +130,10 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
122130
});
123131
};
124132

125-
export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => {
133+
export const NON_AGGREGATABLE = 'non-aggregatable';
134+
export const getDataGridSchemaFromKibanaFieldType = (
135+
field: IFieldType | undefined
136+
): string | undefined => {
126137
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
127138
// To fall back to the default string schema it needs to be undefined.
128139
let schema;
@@ -143,6 +154,10 @@ export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefin
143154
break;
144155
}
145156

157+
if (schema === undefined && field?.aggregatable === false) {
158+
return NON_AGGREGATABLE;
159+
}
160+
146161
return schema;
147162
};
148163

@@ -289,3 +304,17 @@ export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['colum
289304

290305
return sortFn;
291306
};
307+
308+
export const showDataGridColumnChartErrorMessageToast = (
309+
e: any,
310+
toastNotifications: CoreSetup['notifications']['toasts']
311+
) => {
312+
const error = extractErrorMessage(e);
313+
314+
toastNotifications.addDanger(
315+
i18n.translate('xpack.ml.dataGrid.columnChart.ErrorMessageToast', {
316+
defaultMessage: 'An error occurred fetching the histogram charts data: {error}',
317+
values: { error: error !== '' ? error : e },
318+
})
319+
);
320+
};

x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, { memo, useEffect, FC } from 'react';
1010
import { i18n } from '@kbn/i18n';
1111

1212
import {
13+
EuiButtonEmpty,
1314
EuiButtonIcon,
1415
EuiCallOut,
1516
EuiCodeBlock,
@@ -27,6 +28,8 @@ import { INDEX_STATUS } from '../../data_frame_analytics/common';
2728

2829
import { euiDataGridStyle, euiDataGridToolbarSettings } from './common';
2930
import { UseIndexDataReturnType } from './types';
31+
// TODO Fix row hovering + bar highlighting
32+
// import { hoveredRow$ } from './column_chart';
3033

3134
export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
3235
<EuiTitle size="xs">
@@ -54,7 +57,9 @@ type Props = PropsWithHeader | PropsWithoutHeader;
5457
export const DataGrid: FC<Props> = memo(
5558
(props) => {
5659
const {
57-
columns,
60+
chartsVisible,
61+
chartsButtonVisible,
62+
columnsWithCharts,
5863
dataTestSubj,
5964
errorMessage,
6065
invalidSortingColumnns,
@@ -70,9 +75,18 @@ export const DataGrid: FC<Props> = memo(
7075
status,
7176
tableItems: data,
7277
toastNotifications,
78+
toggleChartVisibility,
7379
visibleColumns,
7480
} = props;
7581

82+
// TODO Fix row hovering + bar highlighting
83+
// const getRowProps = (item: any) => {
84+
// return {
85+
// onMouseOver: () => hoveredRow$.next(item),
86+
// onMouseLeave: () => hoveredRow$.next(null),
87+
// };
88+
// };
89+
7690
useEffect(() => {
7791
if (invalidSortingColumnns.length > 0) {
7892
invalidSortingColumnns.forEach((columnId) => {
@@ -162,22 +176,50 @@ export const DataGrid: FC<Props> = memo(
162176
<EuiSpacer size="m" />
163177
</div>
164178
)}
165-
<EuiDataGrid
166-
aria-label={isWithHeader(props) ? props.title : ''}
167-
columns={columns}
168-
columnVisibility={{ visibleColumns, setVisibleColumns }}
169-
gridStyle={euiDataGridStyle}
170-
rowCount={rowCount}
171-
renderCellValue={renderCellValue}
172-
sorting={{ columns: sortingColumns, onSort }}
173-
toolbarVisibility={euiDataGridToolbarSettings}
174-
pagination={{
175-
...pagination,
176-
pageSizeOptions: [5, 10, 25],
177-
onChangeItemsPerPage,
178-
onChangePage,
179-
}}
180-
/>
179+
<div className="mlDataGrid">
180+
<EuiDataGrid
181+
aria-label={isWithHeader(props) ? props.title : ''}
182+
columns={columnsWithCharts.map((c) => {
183+
c.initialWidth = 165;
184+
return c;
185+
})}
186+
columnVisibility={{ visibleColumns, setVisibleColumns }}
187+
gridStyle={euiDataGridStyle}
188+
rowCount={rowCount}
189+
renderCellValue={renderCellValue}
190+
sorting={{ columns: sortingColumns, onSort }}
191+
toolbarVisibility={{
192+
...euiDataGridToolbarSettings,
193+
...(chartsButtonVisible
194+
? {
195+
additionalControls: (
196+
<EuiButtonEmpty
197+
aria-checked={chartsVisible}
198+
className={`euiDataGrid__controlBtn${
199+
chartsVisible ? ' euiDataGrid__controlBtn--active' : ''
200+
}`}
201+
data-test-subj={`${dataTestSubj}HistogramButton`}
202+
size="xs"
203+
iconType="visBarVertical"
204+
color="text"
205+
onClick={toggleChartVisibility}
206+
>
207+
{i18n.translate('xpack.ml.dataGrid.histogramButtonText', {
208+
defaultMessage: 'Histogram charts',
209+
})}
210+
</EuiButtonEmpty>
211+
),
212+
}
213+
: {}),
214+
}}
215+
pagination={{
216+
...pagination,
217+
pageSizeOptions: [5, 10, 25],
218+
onChangeItemsPerPage,
219+
onChangePage,
220+
}}
221+
/>
222+
</div>
181223
</div>
182224
);
183225
},
@@ -186,7 +228,7 @@ export const DataGrid: FC<Props> = memo(
186228

187229
function pickProps(props: Props) {
188230
return [
189-
props.columns,
231+
props.columnsWithCharts,
190232
props.dataTestSubj,
191233
props.errorMessage,
192234
props.invalidSortingColumnns,

x-pack/plugins/ml/public/application/components/data_grid/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ export {
99
getDataGridSchemaFromKibanaFieldType,
1010
getFieldsFromKibanaIndexPattern,
1111
multiColumnSortFactory,
12+
showDataGridColumnChartErrorMessageToast,
1213
useRenderCellValue,
1314
} from './common';
15+
export { fetchChartsData, ChartData } from './use_column_chart';
1416
export { useDataGrid } from './use_data_grid';
1517
export { DataGrid } from './data_grid';
1618
export {

0 commit comments

Comments
 (0)