Skip to content

Commit

Permalink
[ML] Data Grid Histograms (#68359)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
walterra authored Jun 19, 2020
1 parent 639d1e0 commit a489e5f
Show file tree
Hide file tree
Showing 24 changed files with 996 additions and 95 deletions.
16 changes: 2 additions & 14 deletions x-pack/plugins/ml/common/util/group_color_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

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

import { stringHash } from './string_utils';

const COLORS = [
euiVars.euiColorVis0,
euiVars.euiColorVis1,
Expand Down Expand Up @@ -33,17 +35,3 @@ export function tabColor(name: string): string {
return colorMap[name];
}
}

function stringHash(str: string): number {
let hash = 0;
let chr = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise
hash |= 0; // eslint-disable-line no-bitwise
}
return hash < 0 ? hash * -2 : hash;
}
10 changes: 9 additions & 1 deletion x-pack/plugins/ml/common/util/string_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { renderTemplate, getMedianStringLength } from './string_utils';
import { renderTemplate, getMedianStringLength, stringHash } from './string_utils';

const strings: string[] = [
'foo',
Expand Down Expand Up @@ -46,4 +46,12 @@ describe('ML - string utils', () => {
expect(result).toBe(0);
});
});

describe('stringHash', () => {
test('should return a unique number based off a string', () => {
const hash1 = stringHash('the-string-1');
const hash2 = stringHash('the-string-2');
expect(hash1).not.toBe(hash2);
});
});
});
17 changes: 17 additions & 0 deletions x-pack/plugins/ml/common/util/string_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,20 @@ export function getMedianStringLength(strings: string[]) {
const sortedStringLengths = strings.map((s) => s.length).sort((a, b) => a - b);
return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0;
}

/**
* Creates a deterministic number based hash out of a string.
*/
export function stringHash(str: string): number {
let hash = 0;
let chr = 0;
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise
hash |= 0; // eslint-disable-line no-bitwise
}
return hash < 0 ? hash * -2 : hash;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.mlDataGridChart__histogram {
width: 100%;
height: $euiSizeXL + $euiSizeXXL;
}

.mlDataGridChart__legend {
@include euiTextTruncate;
@include euiFontSizeXS;

color: $euiColorMediumShade;
display: block;
overflow-x: hidden;
margin: $euiSizeXS 0px 0px 0px;
font-style: italic;
font-weight: normal;
text-align: left;
}

.mlDataGridChart__legend--numeric {
text-align: right;
}

.mlDataGridChart__legendBoolean {
width: 100%;
td { text-align: center }
}

/* Override to align column header to bottom of cell when no chart is available */
.mlDataGrid .euiDataGridHeaderCell__content {
margin-top: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC } from 'react';
import classNames from 'classnames';

import { BarSeries, Chart, Settings } from '@elastic/charts';
import { EuiDataGridColumn } from '@elastic/eui';

import './column_chart.scss';

import { isUnsupportedChartData, useColumnChart, ChartData } from './use_column_chart';

interface Props {
chartData: ChartData;
columnType: EuiDataGridColumn;
dataTestSubj: string;
}

export const ColumnChart: FC<Props> = ({ chartData, columnType, dataTestSubj }) => {
const { data, legendText, xScaleType } = useColumnChart(chartData, columnType);

return (
<div data-test-subj={dataTestSubj}>
{!isUnsupportedChartData(chartData) && data.length > 0 && (
<div className="mlDataGridChart__histogram" data-test-subj={`${dataTestSubj}-histogram`}>
<Chart>
<Settings
theme={{
background: { color: 'transparent' },
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 1,
},
chartPaddings: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
scales: { barsPadding: 0.1 },
}}
/>
<BarSeries
id="histogram"
name="count"
xScaleType={xScaleType}
yScaleType="linear"
xAccessor="key"
yAccessors={['doc_count']}
styleAccessor={(d) => d.datum.color}
data={data}
/>
</Chart>
</div>
)}
<div
className={classNames('mlDataGridChart__legend', {
'mlDataGridChart__legend--numeric': columnType.schema === 'number',
})}
data-test-subj={`${dataTestSubj}-legend`}
>
{legendText}
</div>
<div data-test-subj={`${dataTestSubj}-id`}>{columnType.id}</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import {
EuiDataGridStyle,
} from '@elastic/eui';

import { i18n } from '@kbn/i18n';

import { CoreSetup } from 'src/core/public';

import {
IndexPattern,
IFieldType,
ES_FIELD_TYPES,
KBN_FIELD_TYPES,
} from '../../../../../../../src/plugins/data/public';

import { extractErrorMessage } from '../../../../common/util/errors';

import {
BASIC_NUMERICAL_TYPES,
EXTENDED_NUMERICAL_TYPES,
Expand All @@ -37,7 +43,7 @@ import { mlFieldFormatService } from '../../services/field_format_service';

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

export const INIT_MAX_COLUMNS = 20;
export const INIT_MAX_COLUMNS = 10;

export const euiDataGridStyle: EuiDataGridStyle = {
border: 'all',
Expand Down Expand Up @@ -102,6 +108,8 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
case 'boolean':
schema = 'boolean';
break;
case 'text':
schema = NON_AGGREGATABLE;
}

if (
Expand All @@ -122,7 +130,10 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
});
};

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

if (schema === undefined && field?.aggregatable === false) {
return NON_AGGREGATABLE;
}

return schema;
};

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

return sortFn;
};

export const showDataGridColumnChartErrorMessageToast = (
e: any,
toastNotifications: CoreSetup['notifications']['toasts']
) => {
const error = extractErrorMessage(e);

toastNotifications.addDanger(
i18n.translate('xpack.ml.dataGrid.columnChart.ErrorMessageToast', {
defaultMessage: 'An error occurred fetching the histogram charts data: {error}',
values: { error: error !== '' ? error : e },
})
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, { memo, useEffect, FC } from 'react';
import { i18n } from '@kbn/i18n';

import {
EuiButtonEmpty,
EuiButtonIcon,
EuiCallOut,
EuiCodeBlock,
Expand All @@ -27,6 +28,8 @@ import { INDEX_STATUS } from '../../data_frame_analytics/common';

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

export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
<EuiTitle size="xs">
Expand Down Expand Up @@ -54,7 +57,9 @@ type Props = PropsWithHeader | PropsWithoutHeader;
export const DataGrid: FC<Props> = memo(
(props) => {
const {
columns,
chartsVisible,
chartsButtonVisible,
columnsWithCharts,
dataTestSubj,
errorMessage,
invalidSortingColumnns,
Expand All @@ -70,9 +75,18 @@ export const DataGrid: FC<Props> = memo(
status,
tableItems: data,
toastNotifications,
toggleChartVisibility,
visibleColumns,
} = props;

// TODO Fix row hovering + bar highlighting
// const getRowProps = (item: any) => {
// return {
// onMouseOver: () => hoveredRow$.next(item),
// onMouseLeave: () => hoveredRow$.next(null),
// };
// };

useEffect(() => {
if (invalidSortingColumnns.length > 0) {
invalidSortingColumnns.forEach((columnId) => {
Expand Down Expand Up @@ -162,22 +176,50 @@ export const DataGrid: FC<Props> = memo(
<EuiSpacer size="m" />
</div>
)}
<EuiDataGrid
aria-label={isWithHeader(props) ? props.title : ''}
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
gridStyle={euiDataGridStyle}
rowCount={rowCount}
renderCellValue={renderCellValue}
sorting={{ columns: sortingColumns, onSort }}
toolbarVisibility={euiDataGridToolbarSettings}
pagination={{
...pagination,
pageSizeOptions: [5, 10, 25],
onChangeItemsPerPage,
onChangePage,
}}
/>
<div className="mlDataGrid">
<EuiDataGrid
aria-label={isWithHeader(props) ? props.title : ''}
columns={columnsWithCharts.map((c) => {
c.initialWidth = 165;
return c;
})}
columnVisibility={{ visibleColumns, setVisibleColumns }}
gridStyle={euiDataGridStyle}
rowCount={rowCount}
renderCellValue={renderCellValue}
sorting={{ columns: sortingColumns, onSort }}
toolbarVisibility={{
...euiDataGridToolbarSettings,
...(chartsButtonVisible
? {
additionalControls: (
<EuiButtonEmpty
aria-checked={chartsVisible}
className={`euiDataGrid__controlBtn${
chartsVisible ? ' euiDataGrid__controlBtn--active' : ''
}`}
data-test-subj={`${dataTestSubj}HistogramButton`}
size="xs"
iconType="visBarVertical"
color="text"
onClick={toggleChartVisibility}
>
{i18n.translate('xpack.ml.dataGrid.histogramButtonText', {
defaultMessage: 'Histogram charts',
})}
</EuiButtonEmpty>
),
}
: {}),
}}
pagination={{
...pagination,
pageSizeOptions: [5, 10, 25],
onChangeItemsPerPage,
onChangePage,
}}
/>
</div>
</div>
);
},
Expand All @@ -186,7 +228,7 @@ export const DataGrid: FC<Props> = memo(

function pickProps(props: Props) {
return [
props.columns,
props.columnsWithCharts,
props.dataTestSubj,
props.errorMessage,
props.invalidSortingColumnns,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export {
getDataGridSchemaFromKibanaFieldType,
getFieldsFromKibanaIndexPattern,
multiColumnSortFactory,
showDataGridColumnChartErrorMessageToast,
useRenderCellValue,
} from './common';
export { fetchChartsData, ChartData } from './use_column_chart';
export { useDataGrid } from './use_data_grid';
export { DataGrid } from './data_grid';
export {
Expand Down
Loading

0 comments on commit a489e5f

Please sign in to comment.