mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Color Range Legend component (#52794)
Introduces ColorRangeLegend, a reusable component to display a color range legend to go along with color coded table cells or visualizations such as heatmaps.
This commit is contained in:
parent
065ca6e041
commit
af1a876f21
8 changed files with 485 additions and 56 deletions
|
@ -0,0 +1,18 @@
|
|||
/* Overrides for d3/svg default styles */
|
||||
.mlColorRangeLegend {
|
||||
text {
|
||||
@include fontSize($euiFontSizeXS - 2px);
|
||||
fill: $euiColorDarkShade;
|
||||
}
|
||||
|
||||
.axis path {
|
||||
fill: none;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: $euiColorMediumShade;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'color_range_legend';
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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, { useEffect, useRef, FC } from 'react';
|
||||
import d3 from 'd3';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
|
||||
const COLOR_RANGE_RESOLUTION = 10;
|
||||
|
||||
interface ColorRangeLegendProps {
|
||||
colorRange: (d: number) => string;
|
||||
justifyTicks?: boolean;
|
||||
showTicks?: boolean;
|
||||
title?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to render a legend for color ranges to be used for color coding
|
||||
* table cells and visualizations.
|
||||
*
|
||||
* This current version supports normalized value ranges (0-1) only.
|
||||
*
|
||||
* @param props ColorRangeLegendProps
|
||||
*/
|
||||
export const ColorRangeLegend: FC<ColorRangeLegendProps> = ({
|
||||
colorRange,
|
||||
justifyTicks = false,
|
||||
showTicks = true,
|
||||
title,
|
||||
width = 250,
|
||||
}) => {
|
||||
const d3Container = useRef<null | SVGSVGElement>(null);
|
||||
|
||||
const scale = d3.range(COLOR_RANGE_RESOLUTION + 1).map(d => ({
|
||||
offset: (d / COLOR_RANGE_RESOLUTION) * 100,
|
||||
stopColor: colorRange(d / COLOR_RANGE_RESOLUTION),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Container.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapperHeight = 32;
|
||||
const wrapperWidth = width;
|
||||
|
||||
// top: 2 — adjust vertical alignment with title text
|
||||
// bottom: 20 — room for axis ticks and labels
|
||||
// left/right: 1 — room for first and last axis tick
|
||||
// when justifyTicks is enabled, the left margin is increased to not cut off the first tick label
|
||||
const margin = { top: 2, bottom: 20, left: justifyTicks || !showTicks ? 1 : 4, right: 1 };
|
||||
|
||||
const legendWidth = wrapperWidth - margin.left - margin.right;
|
||||
const legendHeight = wrapperHeight - margin.top - margin.bottom;
|
||||
|
||||
// remove, then redraw the legend
|
||||
d3.select(d3Container.current)
|
||||
.selectAll('*')
|
||||
.remove();
|
||||
|
||||
const wrapper = d3
|
||||
.select(d3Container.current)
|
||||
.classed('mlColorRangeLegend', true)
|
||||
.attr('width', wrapperWidth)
|
||||
.attr('height', wrapperHeight)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
// append gradient bar
|
||||
const gradient = wrapper
|
||||
.append('defs')
|
||||
.append('linearGradient')
|
||||
.attr('id', 'mlColorRangeGradient')
|
||||
.attr('x1', '0%')
|
||||
.attr('y1', '0%')
|
||||
.attr('x2', '100%')
|
||||
.attr('y2', '0%')
|
||||
.attr('spreadMethod', 'pad');
|
||||
|
||||
scale.forEach(function(d) {
|
||||
gradient
|
||||
.append('stop')
|
||||
.attr('offset', `${d.offset}%`)
|
||||
.attr('stop-color', d.stopColor)
|
||||
.attr('stop-opacity', 1);
|
||||
});
|
||||
|
||||
wrapper
|
||||
.append('rect')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', 0)
|
||||
.attr('width', legendWidth)
|
||||
.attr('height', legendHeight)
|
||||
.style('fill', 'url(#mlColorRangeGradient)');
|
||||
|
||||
const axisScale = d3.scale
|
||||
.linear()
|
||||
.domain([0, 1])
|
||||
.range([0, legendWidth]);
|
||||
|
||||
// Using this formatter ensures we get e.g. `0` and not `0.0`, but still `0.1`, `0.2` etc.
|
||||
const tickFormat = d3.format('');
|
||||
const legendAxis = d3.svg
|
||||
.axis()
|
||||
.scale(axisScale)
|
||||
.orient('bottom')
|
||||
.tickFormat(tickFormat)
|
||||
.tickSize(legendHeight + 4)
|
||||
.ticks(legendWidth / 40);
|
||||
|
||||
wrapper
|
||||
.append('g')
|
||||
.attr('class', 'legend axis')
|
||||
.attr('transform', 'translate(0, 0)')
|
||||
.call(legendAxis);
|
||||
|
||||
// Adjust the alignment of the first and last tick text
|
||||
// so that the tick labels don't overflow the color range.
|
||||
if (justifyTicks || !showTicks) {
|
||||
const text = wrapper.selectAll('text')[0];
|
||||
if (text.length > 1) {
|
||||
d3.select(text[0]).style('text-anchor', 'start');
|
||||
d3.select(text[text.length - 1]).style('text-anchor', 'end');
|
||||
}
|
||||
}
|
||||
|
||||
if (!showTicks) {
|
||||
wrapper.selectAll('.axis line').style('display', 'none');
|
||||
}
|
||||
}, [JSON.stringify(scale), d3Container.current]);
|
||||
|
||||
if (title === undefined) {
|
||||
return <svg ref={d3Container} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<strong>{title}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<svg ref={d3Container} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { ColorRangeLegend } from './color_range_legend';
|
||||
export {
|
||||
colorRangeOptions,
|
||||
colorRangeScaleOptions,
|
||||
useColorRange,
|
||||
COLOR_RANGE,
|
||||
COLOR_RANGE_SCALE,
|
||||
} from './use_color_range';
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { influencerColorScaleFactory } from './use_color_range';
|
||||
|
||||
jest.mock('../../contexts/ui/use_ui_chrome_context');
|
||||
|
||||
describe('useColorRange', () => {
|
||||
test('influencerColorScaleFactory(1)', () => {
|
||||
const influencerColorScale = influencerColorScaleFactory(1);
|
||||
|
||||
expect(influencerColorScale(0)).toBe(0);
|
||||
expect(influencerColorScale(0.1)).toBe(0.1);
|
||||
expect(influencerColorScale(0.2)).toBe(0.2);
|
||||
expect(influencerColorScale(0.3)).toBe(0.3);
|
||||
expect(influencerColorScale(0.4)).toBe(0.4);
|
||||
expect(influencerColorScale(0.5)).toBe(0.5);
|
||||
expect(influencerColorScale(0.6)).toBe(0.6);
|
||||
expect(influencerColorScale(0.7)).toBe(0.7);
|
||||
expect(influencerColorScale(0.8)).toBe(0.8);
|
||||
expect(influencerColorScale(0.9)).toBe(0.9);
|
||||
expect(influencerColorScale(1)).toBe(1);
|
||||
});
|
||||
|
||||
test('influencerColorScaleFactory(2)', () => {
|
||||
const influencerColorScale = influencerColorScaleFactory(2);
|
||||
|
||||
expect(influencerColorScale(0)).toBe(0);
|
||||
expect(influencerColorScale(0.1)).toBe(0);
|
||||
expect(influencerColorScale(0.2)).toBe(0);
|
||||
expect(influencerColorScale(0.3)).toBe(0);
|
||||
expect(influencerColorScale(0.4)).toBe(0);
|
||||
expect(influencerColorScale(0.5)).toBe(0);
|
||||
expect(influencerColorScale(0.6)).toBe(0.04999999999999999);
|
||||
expect(influencerColorScale(0.7)).toBe(0.09999999999999998);
|
||||
expect(influencerColorScale(0.8)).toBe(0.15000000000000002);
|
||||
expect(influencerColorScale(0.9)).toBe(0.2);
|
||||
expect(influencerColorScale(1)).toBe(0.25);
|
||||
});
|
||||
|
||||
test('influencerColorScaleFactory(3)', () => {
|
||||
const influencerColorScale = influencerColorScaleFactory(3);
|
||||
|
||||
expect(influencerColorScale(0)).toBe(0);
|
||||
expect(influencerColorScale(0.1)).toBe(0);
|
||||
expect(influencerColorScale(0.2)).toBe(0);
|
||||
expect(influencerColorScale(0.3)).toBe(0);
|
||||
expect(influencerColorScale(0.4)).toBe(0.05000000000000003);
|
||||
expect(influencerColorScale(0.5)).toBe(0.125);
|
||||
expect(influencerColorScale(0.6)).toBe(0.2);
|
||||
expect(influencerColorScale(0.7)).toBe(0.27499999999999997);
|
||||
expect(influencerColorScale(0.8)).toBe(0.35000000000000003);
|
||||
expect(influencerColorScale(0.9)).toBe(0.425);
|
||||
expect(influencerColorScale(1)).toBe(0.5);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 d3 from 'd3';
|
||||
|
||||
import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useUiChromeContext } from '../../contexts/ui/use_ui_chrome_context';
|
||||
|
||||
/**
|
||||
* Custom color scale factory that takes the amount of feature influencers
|
||||
* into account to adjust the contrast of the color range. This is used for
|
||||
* color coding for outlier detection where the amount of feature influencers
|
||||
* affects the threshold from which the influencers value can actually be
|
||||
* considered influential.
|
||||
*
|
||||
* @param n number of influencers
|
||||
* @returns a function suitable as a preprocessor for d3.scale.linear()
|
||||
*/
|
||||
export const influencerColorScaleFactory = (n: number) => (t: number) => {
|
||||
// for 1 influencer or less we fall back to a plain linear scale.
|
||||
if (n <= 1) {
|
||||
return t;
|
||||
}
|
||||
|
||||
if (t < 1 / n) {
|
||||
return 0;
|
||||
}
|
||||
if (t < 3 / n) {
|
||||
return (n / 4) * (t - 1 / n);
|
||||
}
|
||||
return 0.5 + (t - 3 / n);
|
||||
};
|
||||
|
||||
export enum COLOR_RANGE_SCALE {
|
||||
LINEAR = 'linear',
|
||||
INFLUENCER = 'influencer',
|
||||
SQRT = 'sqrt',
|
||||
}
|
||||
|
||||
/**
|
||||
* Color range scale options in the format for EuiSelect's options prop.
|
||||
*/
|
||||
export const colorRangeScaleOptions = [
|
||||
{
|
||||
value: COLOR_RANGE_SCALE.LINEAR,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.linearScaleLabel', {
|
||||
defaultMessage: 'Linear',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE_SCALE.INFLUENCER,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.influencerScaleLabel', {
|
||||
defaultMessage: 'Influencer custom scale',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE_SCALE.SQRT,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.sqrtScaleLabel', {
|
||||
defaultMessage: 'Sqrt',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export enum COLOR_RANGE {
|
||||
BLUE = 'blue',
|
||||
RED = 'red',
|
||||
RED_GREEN = 'red-green',
|
||||
GREEN_RED = 'green-red',
|
||||
YELLOW_GREEN_BLUE = 'yellow-green-blue',
|
||||
}
|
||||
|
||||
/**
|
||||
* Color range options in the format for EuiSelect's options prop.
|
||||
*/
|
||||
export const colorRangeOptions = [
|
||||
{
|
||||
value: COLOR_RANGE.BLUE,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.blueColorRangeLabel', {
|
||||
defaultMessage: 'Blue',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE.RED,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.redColorRangeLabel', {
|
||||
defaultMessage: 'Red',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE.RED_GREEN,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.redGreenColorRangeLabel', {
|
||||
defaultMessage: 'Red - Green',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE.GREEN_RED,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel', {
|
||||
defaultMessage: 'Green - Red',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_RANGE.YELLOW_GREEN_BLUE,
|
||||
text: i18n.translate('xpack.ml.components.colorRangeLegend.yellowGreenBlueColorRangeLabel', {
|
||||
defaultMessage: 'Yellow - Green - Blue',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* A custom Yellow-Green-Blue color range to demonstrate the support
|
||||
* for more complex ranges with more than two colors.
|
||||
*/
|
||||
const coloursYGB = [
|
||||
'#FFFFDD',
|
||||
'#AAF191',
|
||||
'#80D385',
|
||||
'#61B385',
|
||||
'#3E9583',
|
||||
'#217681',
|
||||
'#285285',
|
||||
'#1F2D86',
|
||||
'#000086',
|
||||
];
|
||||
const colourRangeYGB = d3.range(0, 1, 1.0 / (coloursYGB.length - 1));
|
||||
colourRangeYGB.push(1);
|
||||
|
||||
const colorDomains = {
|
||||
[COLOR_RANGE.BLUE]: [0, 1],
|
||||
[COLOR_RANGE.RED]: [0, 1],
|
||||
[COLOR_RANGE.RED_GREEN]: [0, 1],
|
||||
[COLOR_RANGE.GREEN_RED]: [0, 1],
|
||||
[COLOR_RANGE.YELLOW_GREEN_BLUE]: colourRangeYGB,
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to get a d3 based color range to be used for color coding in table cells.
|
||||
*
|
||||
* @param colorRange COLOR_RANGE enum.
|
||||
* @param colorRangeScale COLOR_RANGE_SCALE enum.
|
||||
* @param featureCount
|
||||
*/
|
||||
export const useColorRange = (
|
||||
colorRange = COLOR_RANGE.BLUE,
|
||||
colorRangeScale = COLOR_RANGE_SCALE.LINEAR,
|
||||
featureCount = 1
|
||||
) => {
|
||||
const euiTheme = useUiChromeContext()
|
||||
.getUiSettingsClient()
|
||||
.get('theme:darkMode')
|
||||
? euiThemeDark
|
||||
: euiThemeLight;
|
||||
|
||||
const colorRanges = {
|
||||
[COLOR_RANGE.BLUE]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)],
|
||||
[COLOR_RANGE.RED]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorDanger)],
|
||||
[COLOR_RANGE.RED_GREEN]: ['red', 'green'],
|
||||
[COLOR_RANGE.GREEN_RED]: ['green', 'red'],
|
||||
[COLOR_RANGE.YELLOW_GREEN_BLUE]: coloursYGB,
|
||||
};
|
||||
|
||||
const linearScale = d3.scale
|
||||
.linear()
|
||||
.domain(colorDomains[colorRange])
|
||||
// typings for .range() incorrectly don't allow passing in a color extent.
|
||||
// @ts-ignore
|
||||
.range(colorRanges[colorRange]);
|
||||
const influencerColorScale = influencerColorScaleFactory(featureCount);
|
||||
const influencerScaleLinearWrapper = (n: number) => linearScale(influencerColorScale(n));
|
||||
|
||||
const scaleTypes = {
|
||||
[COLOR_RANGE_SCALE.LINEAR]: linearScale,
|
||||
[COLOR_RANGE_SCALE.INFLUENCER]: influencerScaleLinearWrapper,
|
||||
[COLOR_RANGE_SCALE.SQRT]: d3.scale
|
||||
.sqrt()
|
||||
.domain(colorDomains[colorRange])
|
||||
// typings for .range() incorrectly don't allow passing in a color extent.
|
||||
// @ts-ignore
|
||||
.range(colorRanges[colorRange]),
|
||||
};
|
||||
|
||||
return scaleTypes[colorRangeScale];
|
||||
};
|
|
@ -4,13 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC, Fragment, useEffect, useState } from 'react';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import d3 from 'd3';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonIcon,
|
||||
|
@ -18,7 +16,6 @@ import {
|
|||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
|
@ -30,9 +27,12 @@ import {
|
|||
Query,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
|
||||
import {
|
||||
useColorRange,
|
||||
ColorRangeLegend,
|
||||
COLOR_RANGE,
|
||||
COLOR_RANGE_SCALE,
|
||||
} from '../../../../../components/color_range_legend';
|
||||
import {
|
||||
ColumnType,
|
||||
mlInMemoryTableBasicFactory,
|
||||
|
@ -41,8 +41,6 @@ import {
|
|||
SORT_DIRECTION,
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context';
|
||||
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
|
||||
|
@ -67,16 +65,6 @@ import {
|
|||
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
|
||||
import { SavedSearchQuery } from '../../../../../contexts/kibana';
|
||||
|
||||
const customColorScaleFactory = (n: number) => (t: number) => {
|
||||
if (t < 1 / n) {
|
||||
return 0;
|
||||
}
|
||||
if (t < 3 / n) {
|
||||
return (n / 4) * (t - 1 / n);
|
||||
}
|
||||
return 0.5 + (t - 3 / n);
|
||||
};
|
||||
|
||||
const FEATURE_INFLUENCE = 'feature_influence';
|
||||
|
||||
interface GetDataFrameAnalyticsResponse {
|
||||
|
@ -102,6 +90,16 @@ interface Props {
|
|||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
}
|
||||
|
||||
const getFeatureCount = (jobConfig?: DataFrameAnalyticsConfig, tableItems: TableItem[] = []) => {
|
||||
if (jobConfig === undefined || tableItems.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.keys(tableItems[0]).filter(key =>
|
||||
key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`)
|
||||
).length;
|
||||
};
|
||||
|
||||
export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
|
||||
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
|
||||
|
||||
|
@ -126,12 +124,6 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
|
|||
})();
|
||||
}, []);
|
||||
|
||||
const euiTheme = useUiChromeContext()
|
||||
.getUiSettingsClient()
|
||||
.get('theme:darkMode')
|
||||
? euiThemeDark
|
||||
: euiThemeLight;
|
||||
|
||||
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
|
||||
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
|
||||
|
||||
|
@ -169,23 +161,13 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
|
|||
|
||||
const columns: Array<ColumnType<TableItem>> = [];
|
||||
|
||||
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
|
||||
// table cell color coding takes into account:
|
||||
// - whether the theme is dark/light
|
||||
// - the number of analysis features
|
||||
// based on that
|
||||
const cellBgColorScale = d3.scale
|
||||
.linear()
|
||||
.domain([0, 1])
|
||||
// typings for .range() incorrectly don't allow passing in a color extent.
|
||||
// @ts-ignore
|
||||
.range([d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)]);
|
||||
const featureCount = Object.keys(tableItems[0]).filter(key =>
|
||||
key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`)
|
||||
).length;
|
||||
const customScale = customColorScaleFactory(featureCount);
|
||||
const cellBgColor = (n: number) => cellBgColorScale(customScale(n));
|
||||
const cellBgColor = useColorRange(
|
||||
COLOR_RANGE.BLUE,
|
||||
COLOR_RANGE_SCALE.INFLUENCER,
|
||||
getFeatureCount(jobConfig, tableItems)
|
||||
);
|
||||
|
||||
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
|
||||
columns.push(
|
||||
...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => {
|
||||
const column: ColumnType<TableItem> = {
|
||||
|
@ -504,21 +486,34 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
|
|||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && (
|
||||
<Fragment>
|
||||
{tableItems.length === SEARCH_SIZE && (
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.exploration.documentsShownHelpText',
|
||||
{
|
||||
defaultMessage: 'Showing first {searchSize} documents',
|
||||
values: { searchSize: SEARCH_SIZE },
|
||||
}
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
{tableItems.length === SEARCH_SIZE && (
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.exploration.documentsShownHelpText',
|
||||
{
|
||||
defaultMessage: 'Showing first {searchSize} documents',
|
||||
values: { searchSize: SEARCH_SIZE },
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
)}
|
||||
>
|
||||
<Fragment />
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ColorRangeLegend
|
||||
colorRange={cellBgColor}
|
||||
title={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle',
|
||||
{
|
||||
defaultMessage: 'Feature influence score',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<MlInMemoryTableBasic
|
||||
allowNeutralSort={false}
|
||||
className="mlDataFrameAnalyticsExploration"
|
||||
|
@ -534,7 +529,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
|
|||
search={search}
|
||||
error={tableError}
|
||||
/>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
@import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly
|
||||
@import 'components/chart_tooltip/index';
|
||||
@import 'components/color_range_legend/index';
|
||||
@import 'components/controls/index';
|
||||
@import 'components/entity_cell/index';
|
||||
@import 'components/field_title_bar/index';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue