mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[XY] Detailed tooltip. (#131116)
* Added detailed tooltip. * Added tests. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
be3c3b52db
commit
ca36c6dff8
32 changed files with 1842 additions and 110 deletions
|
@ -128,6 +128,10 @@ export const commonXYArgs: CommonXYFn['args'] = {
|
|||
types: ['string'],
|
||||
help: strings.getAriaLabelHelp(),
|
||||
},
|
||||
detailedTooltip: {
|
||||
types: ['boolean'],
|
||||
help: strings.getDetailedTooltipHelp(),
|
||||
},
|
||||
showTooltip: {
|
||||
types: ['boolean'],
|
||||
default: true,
|
||||
|
|
|
@ -121,6 +121,10 @@ export const strings = {
|
|||
i18n.translate('expressionXY.xyVis.ariaLabel.help', {
|
||||
defaultMessage: 'Specifies the aria label of the xy chart',
|
||||
}),
|
||||
getDetailedTooltipHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.detailedTooltip.help', {
|
||||
defaultMessage: 'Show detailed tooltip',
|
||||
}),
|
||||
getShowTooltipHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.showTooltip.help', {
|
||||
defaultMessage: 'Show tooltip',
|
||||
|
|
|
@ -12,7 +12,8 @@ import type { PaletteOutput } from '@kbn/coloring';
|
|||
import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin';
|
||||
import { LegendSize } from '@kbn/visualizations-plugin/public';
|
||||
import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common';
|
||||
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
|
||||
import {
|
||||
AxisExtentModes,
|
||||
FillStyles,
|
||||
|
@ -220,6 +221,7 @@ export interface XYArgs extends DataLayerArgs {
|
|||
minTimeBarInterval?: string;
|
||||
splitRowAccessor?: ExpressionValueVisDimension | string;
|
||||
splitColumnAccessor?: ExpressionValueVisDimension | string;
|
||||
detailedTooltip?: boolean;
|
||||
orderBucketsBySum?: boolean;
|
||||
showTooltip: boolean;
|
||||
}
|
||||
|
@ -247,6 +249,7 @@ export interface LayeredXYArgs {
|
|||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
detailedTooltip?: boolean;
|
||||
addTimeMarker?: boolean;
|
||||
markSizeRatio?: number;
|
||||
minTimeBarInterval?: string;
|
||||
|
@ -282,6 +285,7 @@ export interface XYProps {
|
|||
minTimeBarInterval?: string;
|
||||
splitRowAccessor?: ExpressionValueVisDimension | string;
|
||||
splitColumnAccessor?: ExpressionValueVisDimension | string;
|
||||
detailedTooltip?: boolean;
|
||||
orderBucketsBySum?: boolean;
|
||||
showTooltip: boolean;
|
||||
}
|
||||
|
|
|
@ -330,6 +330,7 @@ exports[`XYChart component it renders area 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -786,6 +787,24 @@ exports[`XYChart component it renders area 1`] = `
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -882,6 +901,7 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -1338,6 +1358,24 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -1434,6 +1472,7 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -1890,6 +1929,24 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -1986,6 +2043,7 @@ exports[`XYChart component it renders line 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -2442,6 +2500,24 @@ exports[`XYChart component it renders line 1`] = `
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -2538,6 +2614,7 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -2994,6 +3071,24 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
shouldShowValueLabels={false}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -3090,6 +3185,7 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -3546,6 +3642,24 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
shouldShowValueLabels={false}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -3642,6 +3756,7 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -4098,6 +4213,24 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
shouldShowValueLabels={false}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -4194,6 +4327,7 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -4285,6 +4419,19 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
},
|
||||
]
|
||||
}
|
||||
fieldFormats={
|
||||
Object {
|
||||
"b": Object {
|
||||
"id": "number",
|
||||
"params": Object {
|
||||
"pattern": "000,0",
|
||||
},
|
||||
},
|
||||
"c": Object {
|
||||
"id": "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
formatFactory={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -4905,6 +5052,28 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {
|
||||
"b": "b",
|
||||
},
|
||||
"splitRowTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -5001,6 +5170,7 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -5092,6 +5262,16 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
},
|
||||
]
|
||||
}
|
||||
fieldFormats={
|
||||
Object {
|
||||
"b": Object {
|
||||
"id": "number",
|
||||
"params": Object {
|
||||
"pattern": "000,0",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
formatFactory={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -5711,6 +5891,26 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {
|
||||
"b": "b",
|
||||
},
|
||||
"splitRowTitles": Object {},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
@ -5807,6 +6007,7 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
|
@ -5898,6 +6099,16 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
},
|
||||
]
|
||||
}
|
||||
fieldFormats={
|
||||
Object {
|
||||
"b": Object {
|
||||
"id": "number",
|
||||
"params": Object {
|
||||
"pattern": "000,0",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
formatFactory={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -6517,6 +6728,26 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
shouldShowValueLabels={true}
|
||||
syncColors={false}
|
||||
timeZone="UTC"
|
||||
titles={
|
||||
Object {
|
||||
"first": Object {
|
||||
"splitColumnTitles": Object {},
|
||||
"splitRowTitles": Object {
|
||||
"b": "b",
|
||||
},
|
||||
"splitSeriesTitles": Object {
|
||||
"d": "ColD",
|
||||
},
|
||||
"xTitles": Object {
|
||||
"c": "c",
|
||||
},
|
||||
"yTitles": Object {
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
valueLabels="hide"
|
||||
yAxesConfiguration={
|
||||
Array [
|
||||
|
|
|
@ -32,9 +32,11 @@ import {
|
|||
GroupsConfiguration,
|
||||
getSeriesProps,
|
||||
DatatablesWithFormatInfo,
|
||||
LayersAccessorsTitles,
|
||||
} from '../helpers';
|
||||
|
||||
interface Props {
|
||||
titles?: LayersAccessorsTitles;
|
||||
layers: CommonXYDataLayerConfig[];
|
||||
formatFactory: FormatFactory;
|
||||
chartHasMoreThanOneBarSeries?: boolean;
|
||||
|
@ -54,6 +56,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const DataLayers: FC<Props> = ({
|
||||
titles = {},
|
||||
layers,
|
||||
endValue,
|
||||
timeZone,
|
||||
|
@ -95,6 +98,7 @@ export const DataLayers: FC<Props> = ({
|
|||
|
||||
const seriesProps = getSeriesProps({
|
||||
layer,
|
||||
titles: titles[layer.layerId],
|
||||
accessor: yColumnId,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
colorAssignments,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Position } from '@elastic/charts';
|
|||
import { ReferenceLineLayerConfig } from '../../../common/types';
|
||||
import { getGroupId } from './utils';
|
||||
import { ReferenceLineAnnotations } from './reference_line_annotations';
|
||||
import { LayerAccessorsTitles } from '../../helpers';
|
||||
|
||||
interface ReferenceLineLayerProps {
|
||||
layer: ReferenceLineLayerConfig;
|
||||
|
@ -20,6 +21,7 @@ interface ReferenceLineLayerProps {
|
|||
paddingMap: Partial<Record<Position, number>>;
|
||||
axesMap: Record<'left' | 'right', boolean>;
|
||||
isHorizontal: boolean;
|
||||
titles?: LayerAccessorsTitles;
|
||||
}
|
||||
|
||||
export const ReferenceLineLayer: FC<ReferenceLineLayerProps> = ({
|
||||
|
@ -28,6 +30,7 @@ export const ReferenceLineLayer: FC<ReferenceLineLayerProps> = ({
|
|||
paddingMap,
|
||||
axesMap,
|
||||
isHorizontal,
|
||||
titles,
|
||||
}) => {
|
||||
if (!layer.yConfig) {
|
||||
return null;
|
||||
|
@ -54,7 +57,7 @@ export const ReferenceLineLayer: FC<ReferenceLineLayerProps> = ({
|
|||
const groupId = getGroupId(axisMode);
|
||||
|
||||
const formatter = formatters[groupId || 'bottom'];
|
||||
const name = columnToLabelMap[yConfig.forAccessor];
|
||||
const name = columnToLabelMap[yConfig.forAccessor] ?? titles?.yTitles?.[yConfig.forAccessor];
|
||||
const value = row[yConfig.forAccessor];
|
||||
const yConfigsWithSameDirection = groupedByDirection[yConfig.fill!];
|
||||
const indexFromSameType = yConfigsWithSameDirection.findIndex(
|
||||
|
|
|
@ -12,7 +12,7 @@ import React from 'react';
|
|||
import { Position } from '@elastic/charts';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { CommonXYReferenceLineLayerConfig, ReferenceLineConfig } from '../../../common/types';
|
||||
import { isReferenceLine } from '../../helpers';
|
||||
import { isReferenceLine, LayersAccessorsTitles } from '../../helpers';
|
||||
import { ReferenceLineLayer } from './reference_line_layer';
|
||||
import { ReferenceLine } from './reference_line';
|
||||
import { getNextValuesForReferenceLines } from './utils';
|
||||
|
@ -23,9 +23,10 @@ export interface ReferenceLinesProps {
|
|||
axesMap: Record<'left' | 'right', boolean>;
|
||||
isHorizontal: boolean;
|
||||
paddingMap: Partial<Record<Position, number>>;
|
||||
titles?: LayersAccessorsTitles;
|
||||
}
|
||||
|
||||
export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => {
|
||||
export const ReferenceLines = ({ layers, titles = {}, ...rest }: ReferenceLinesProps) => {
|
||||
const referenceLines = layers.filter((layer): layer is ReferenceLineConfig =>
|
||||
isReferenceLine(layer)
|
||||
);
|
||||
|
@ -45,7 +46,8 @@ export const ReferenceLines = ({ layers, ...rest }: ReferenceLinesProps) => {
|
|||
return <ReferenceLine key={key} layer={layer} {...rest} nextValue={nextValue} />;
|
||||
}
|
||||
|
||||
return <ReferenceLineLayer key={key} layer={layer} {...rest} />;
|
||||
const layerTitles = titles[layer.layerId];
|
||||
return <ReferenceLineLayer key={key} layer={layer} {...rest} titles={layerTitles} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -9,8 +9,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { GroupBy, SmallMultiples, Predicate } from '@elastic/charts';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import {
|
||||
getAccessorByDimension,
|
||||
getColumnByAccessor,
|
||||
} from '@kbn/visualizations-plugin/common/utils';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { FormatFactory } from '../types';
|
||||
|
||||
interface SplitChartProps {
|
||||
|
@ -18,6 +22,7 @@ interface SplitChartProps {
|
|||
splitRowAccessor?: ExpressionValueVisDimension | string;
|
||||
columns: Datatable['columns'];
|
||||
formatFactory: FormatFactory;
|
||||
fieldFormats: Record<string, SerializedFieldFormat | undefined>;
|
||||
}
|
||||
|
||||
const SPLIT_COLUMN = '__split_column__';
|
||||
|
@ -27,15 +32,16 @@ export const SplitChart = ({
|
|||
splitColumnAccessor,
|
||||
splitRowAccessor,
|
||||
columns,
|
||||
fieldFormats,
|
||||
formatFactory,
|
||||
}: SplitChartProps) => {
|
||||
const format = useCallback(
|
||||
(value: unknown, accessor: ExpressionValueVisDimension | string) => {
|
||||
const formatParams = getFormatByAccessor(accessor, columns);
|
||||
const formatter = formatParams ? formatFactory(formatParams) : formatFactory();
|
||||
const formatParams = fieldFormats[getAccessorByDimension(accessor, columns)];
|
||||
const formatter = formatFactory(formatParams);
|
||||
return formatter.convert(value);
|
||||
},
|
||||
[columns, formatFactory]
|
||||
[columns, formatFactory, fieldFormats]
|
||||
);
|
||||
|
||||
const getData = useCallback(
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EndzoneTooltipHeader should render endzone tooltip with value, if it is specified 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="detailedTooltip__header--partial"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="iInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
The selected time range does not include this entire bucket. It might contain partial data.
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="xs"
|
||||
/>
|
||||
<p>
|
||||
some value
|
||||
</p>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`EndzoneTooltipHeader should render endzone tooltip without value, if it is not specified 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="detailedTooltip__header--partial"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="iInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
The selected time range does not include this entire bucket. It might contain partial data.
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,341 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tooltip should render plain tooltip 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-some value-0"
|
||||
label="x-title"
|
||||
value="formatted-number-some value"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-some value-1"
|
||||
label="y-title"
|
||||
value="formatted-string-some value"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-3"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-4"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip with partial buckets 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<div
|
||||
className="detailedTooltip__header"
|
||||
>
|
||||
<EndzoneTooltipHeader />
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-3"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-4"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip with partial buckets 2`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<div
|
||||
className="detailedTooltip__header"
|
||||
>
|
||||
<EndzoneTooltipHeader />
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-3"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-4"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip with xDomain 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-3"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-4"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without split-column-values 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-3"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without split-row-values 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-2"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-3"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without splitAccessors-values 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-1"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-2"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-3"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without x-value 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-0"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-1"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-2"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-3"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without x-value 2`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="y-title-formatted-string-10-0"
|
||||
label="y-title"
|
||||
value="formatted-string-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-1"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-2"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-3"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Tooltip should render tooltip without y-value 1`] = `
|
||||
<div
|
||||
className="detailedTooltip"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<TooltipRow
|
||||
key="x-title-formatted-number-10-0"
|
||||
label="x-title"
|
||||
value="formatted-number-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-series-title-formatted-date-10-1"
|
||||
label="split-series-title"
|
||||
value="formatted-date-10"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-column-title-d-2"
|
||||
label="split-column-title"
|
||||
value="d"
|
||||
/>
|
||||
<TooltipRow
|
||||
key="split-row-title-d-3"
|
||||
label="split-row-title"
|
||||
value="d"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TooltipHeader should render plain value at the header 1`] = `
|
||||
<Fragment>
|
||||
formatted-15
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TooltipRow should render label and value if both are specified 1`] = `
|
||||
<tr>
|
||||
<td
|
||||
className="detailedTooltip__label"
|
||||
>
|
||||
<div
|
||||
className="detailedTooltip__labelContainer"
|
||||
>
|
||||
tooltip
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="detailedTooltip__value"
|
||||
>
|
||||
<div
|
||||
className="detailedTooltip__valueContainer"
|
||||
>
|
||||
0
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { EndzoneTooltipHeader } from './endzone_tooltip_header';
|
||||
|
||||
describe('EndzoneTooltipHeader', () => {
|
||||
it('should render endzone tooltip with value, if it is specified', () => {
|
||||
const endzoneTooltip = shallow(<EndzoneTooltipHeader value={'some value'} />);
|
||||
expect(endzoneTooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render endzone tooltip without value, if it is not specified', () => {
|
||||
const endzoneTooltip = shallow(<EndzoneTooltipHeader />);
|
||||
expect(endzoneTooltip).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export interface EndzoneTooltipHeaderProps {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const EndzoneTooltipHeader: FC<EndzoneTooltipHeaderProps> = ({ value }) => (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="detailedTooltip__header--partial"
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('expressionXY.partialData.bucketTooltipText', {
|
||||
defaultMessage:
|
||||
'The selected time range does not include this entire bucket. It might contain partial data.',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{value !== undefined && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<p>{value}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { Tooltip } from './tooltip';
|
||||
export { TooltipHeader } from './tooltip_header';
|
||||
export { EndzoneTooltipHeader } from './endzone_tooltip_header';
|
|
@ -0,0 +1,39 @@
|
|||
.detailedTooltip {
|
||||
@include euiToolTipStyle('s');
|
||||
pointer-events: none;
|
||||
max-width: $euiSizeXL * 10;
|
||||
overflow: hidden;
|
||||
padding: $euiSizeS;
|
||||
|
||||
table {
|
||||
td,
|
||||
th {
|
||||
text-align: left;
|
||||
padding: $euiSizeXS;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailedTooltip__header {
|
||||
> :last-child {
|
||||
margin-bottom: $euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
.detailedTooltip__labelContainer,
|
||||
.detailedTooltip__valueContainer {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.detailedTooltip__label {
|
||||
font-weight: $euiFontWeightMedium;
|
||||
color: shade($euiColorGhost, 20%);
|
||||
}
|
||||
|
||||
.detailedTooltip__header--partial {
|
||||
font-weight: $euiFontWeightRegular;
|
||||
min-width: $euiSize * 12;
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import { Tooltip } from './tooltip';
|
||||
import { generateSeriesId, LayersAccessorsTitles, LayersFieldFormats } from '../../helpers';
|
||||
import { XYChartSeriesIdentifier } from '@elastic/charts';
|
||||
import { sampleArgs, sampleLayer } from '../../../common/__mocks__';
|
||||
import { FieldFormat, FormatFactory } from '@kbn/field-formats-plugin/common';
|
||||
|
||||
const getSeriesIdentifier = ({
|
||||
layerId,
|
||||
xAccessor,
|
||||
yAccessor,
|
||||
splitAccessor,
|
||||
splitRowAccessor,
|
||||
splitColumnAccessor,
|
||||
splitAccessors,
|
||||
}: {
|
||||
layerId: string;
|
||||
xAccessor?: string;
|
||||
yAccessor?: string;
|
||||
splitRowAccessor?: string;
|
||||
splitAccessor?: string;
|
||||
splitColumnAccessor?: string;
|
||||
splitAccessors: Map<number | string, number | string>;
|
||||
}): XYChartSeriesIdentifier => ({
|
||||
specId: generateSeriesId({ layerId, xAccessor, splitAccessor }, yAccessor),
|
||||
yAccessor: yAccessor ?? 'a',
|
||||
splitAccessors,
|
||||
seriesKeys: [],
|
||||
key: '1',
|
||||
smVerticalAccessorValue: splitColumnAccessor,
|
||||
smHorizontalAccessorValue: splitRowAccessor,
|
||||
});
|
||||
|
||||
describe('Tooltip', () => {
|
||||
const { data } = sampleArgs();
|
||||
const { layerId, xAccessor, splitAccessor, accessors } = sampleLayer;
|
||||
const splitAccessors = new Map();
|
||||
splitAccessors.set(splitAccessor, '10');
|
||||
|
||||
const accessor = accessors[0] as string;
|
||||
const splitRowAccessor = 'd';
|
||||
const splitColumnAccessor = 'd';
|
||||
|
||||
const seriesIdentifier = getSeriesIdentifier({
|
||||
layerId,
|
||||
yAccessor: accessor,
|
||||
xAccessor: xAccessor as string,
|
||||
splitAccessor: splitAccessor as string,
|
||||
splitAccessors,
|
||||
splitRowAccessor,
|
||||
splitColumnAccessor,
|
||||
});
|
||||
|
||||
const header = {
|
||||
value: 'some value',
|
||||
label: 'some label',
|
||||
formattedValue: 'formatted value',
|
||||
color: '#fff',
|
||||
isHighlighted: true,
|
||||
isVisible: true,
|
||||
seriesIdentifier,
|
||||
};
|
||||
|
||||
const titles: LayersAccessorsTitles = {
|
||||
[layerId]: {
|
||||
xTitles: { [xAccessor as string]: 'x-title' },
|
||||
yTitles: { [accessor]: 'y-title' },
|
||||
splitSeriesTitles: { [splitAccessor as string]: 'split-series-title' },
|
||||
splitRowTitles: { [splitRowAccessor]: 'split-row-title' },
|
||||
splitColumnTitles: { [splitColumnAccessor]: 'split-column-title' },
|
||||
},
|
||||
};
|
||||
|
||||
const fieldFormats: LayersFieldFormats = {
|
||||
[layerId]: {
|
||||
xAccessors: { [xAccessor as string]: { id: 'number' } },
|
||||
yAccessors: { [accessor]: { id: 'string' } },
|
||||
splitSeriesAccessors: { [splitAccessor as string]: { id: 'date' } },
|
||||
splitRowAccessors: { [splitRowAccessor]: { id: 'number' } },
|
||||
splitColumnAccessors: { [splitColumnAccessor]: { id: 'number' } },
|
||||
},
|
||||
};
|
||||
|
||||
const formatFactory: FormatFactory = (format) =>
|
||||
({
|
||||
convert: (value) => `formatted-${format?.id}-${value}`,
|
||||
} as FieldFormat);
|
||||
|
||||
it('should render plain tooltip', () => {
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={header}
|
||||
values={[header]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip with xDomain', () => {
|
||||
const headerWithValue = { ...header, value: 10 };
|
||||
const xDomain = { min: 0, max: 1000 };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={headerWithValue}
|
||||
values={[headerWithValue]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
xDomain={xDomain}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip with partial buckets', () => {
|
||||
const headerWithValue = { ...header, value: 10 };
|
||||
const xDomain = { min: 15, max: 1000 };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={headerWithValue}
|
||||
values={[headerWithValue]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
xDomain={xDomain}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
|
||||
const xDomain2 = { min: 5, max: 1000, minInterval: 995 };
|
||||
|
||||
const tooltip2 = shallow(
|
||||
<Tooltip
|
||||
header={headerWithValue}
|
||||
values={[headerWithValue]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
xDomain={xDomain2}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip2).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip without x-value', () => {
|
||||
const value = { ...header, value: 10 };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={null}
|
||||
values={[value]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
|
||||
const seriesIdentifierWithoutX = getSeriesIdentifier({
|
||||
layerId,
|
||||
yAccessor: accessor,
|
||||
splitAccessor: splitAccessor as string,
|
||||
splitAccessors,
|
||||
splitRowAccessor,
|
||||
splitColumnAccessor,
|
||||
});
|
||||
|
||||
const value2 = { ...header, value: 10, seriesIdentifier: seriesIdentifierWithoutX };
|
||||
|
||||
const tooltip2 = shallow(
|
||||
<Tooltip
|
||||
header={value2}
|
||||
values={[value2]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip2).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip without y-value', () => {
|
||||
const seriesIdentifierWithoutY = getSeriesIdentifier({
|
||||
layerId,
|
||||
xAccessor: xAccessor as string,
|
||||
splitAccessor: splitAccessor as string,
|
||||
splitAccessors,
|
||||
splitRowAccessor,
|
||||
splitColumnAccessor,
|
||||
});
|
||||
|
||||
const value = { ...header, value: 10, seriesIdentifier: seriesIdentifierWithoutY };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={value}
|
||||
values={[value]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip without splitAccessors-values', () => {
|
||||
const seriesIdentifierWithoutSplitAccessors = getSeriesIdentifier({
|
||||
layerId,
|
||||
xAccessor: xAccessor as string,
|
||||
yAccessor: accessor,
|
||||
splitAccessors: new Map(),
|
||||
splitRowAccessor,
|
||||
splitColumnAccessor,
|
||||
});
|
||||
|
||||
const value = { ...header, value: 10, seriesIdentifier: seriesIdentifierWithoutSplitAccessors };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={value}
|
||||
values={[value]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor, splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip without split-row-values', () => {
|
||||
const seriesIdentifierWithoutSplitRow = getSeriesIdentifier({
|
||||
layerId,
|
||||
xAccessor: xAccessor as string,
|
||||
yAccessor: accessor,
|
||||
splitAccessor: splitAccessor as string,
|
||||
splitAccessors,
|
||||
splitColumnAccessor,
|
||||
});
|
||||
|
||||
const value = { ...header, value: 10, seriesIdentifier: seriesIdentifierWithoutSplitRow };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={value}
|
||||
values={[value]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitColumnAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tooltip without split-column-values', () => {
|
||||
const seriesIdentifierWithoutSplitColumn = getSeriesIdentifier({
|
||||
layerId,
|
||||
xAccessor: xAccessor as string,
|
||||
yAccessor: accessor,
|
||||
splitAccessor: splitAccessor as string,
|
||||
splitAccessors,
|
||||
splitRowAccessor,
|
||||
});
|
||||
|
||||
const value = { ...header, value: 10, seriesIdentifier: seriesIdentifierWithoutSplitColumn };
|
||||
|
||||
const tooltip = shallow(
|
||||
<Tooltip
|
||||
header={value}
|
||||
values={[value]}
|
||||
fieldFormats={fieldFormats}
|
||||
titles={titles}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={{ [layerId]: { table: data, formattedColumns: {} } }}
|
||||
splitAccessors={{ splitRowAccessor }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tooltip).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { TooltipInfo, XYChartSeriesIdentifier } from '@elastic/charts';
|
||||
import { FormatFactory } from '@kbn/field-formats-plugin/common';
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
DatatablesWithFormatInfo,
|
||||
getMetaFromSeriesId,
|
||||
LayersAccessorsTitles,
|
||||
LayersFieldFormats,
|
||||
} from '../../helpers';
|
||||
import { XDomain } from '../x_domain';
|
||||
import { EndzoneTooltipHeader } from './endzone_tooltip_header';
|
||||
import { TooltipData, TooltipRow } from './tooltip_row';
|
||||
import { isEndzoneBucket } from './utils';
|
||||
|
||||
import './tooltip.scss';
|
||||
|
||||
type Props = TooltipInfo & {
|
||||
xDomain?: XDomain;
|
||||
fieldFormats: LayersFieldFormats;
|
||||
titles?: LayersAccessorsTitles;
|
||||
formatFactory: FormatFactory;
|
||||
formattedDatatables: DatatablesWithFormatInfo;
|
||||
splitAccessors?: {
|
||||
splitRowAccessor?: string;
|
||||
splitColumnAccessor?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const Tooltip: FC<Props> = ({
|
||||
header,
|
||||
values,
|
||||
fieldFormats,
|
||||
titles = {},
|
||||
formatFactory,
|
||||
formattedDatatables,
|
||||
splitAccessors,
|
||||
xDomain,
|
||||
}) => {
|
||||
const pickedValue = values.find(({ isHighlighted }) => isHighlighted);
|
||||
|
||||
if (!pickedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: TooltipData[] = [];
|
||||
const seriesIdentifier = pickedValue.seriesIdentifier as XYChartSeriesIdentifier;
|
||||
const { layerId, xAccessor, yAccessor } = getMetaFromSeriesId(seriesIdentifier.specId);
|
||||
const { formattedColumns } = formattedDatatables[layerId];
|
||||
const layerTitles = titles[layerId];
|
||||
const layerFormats = fieldFormats[layerId];
|
||||
let headerFormatter;
|
||||
if (header && xAccessor) {
|
||||
headerFormatter = formattedColumns[xAccessor]
|
||||
? null
|
||||
: formatFactory(layerFormats.xAccessors[xAccessor]);
|
||||
data.push({
|
||||
label: layerTitles?.xTitles?.[xAccessor],
|
||||
value: headerFormatter ? headerFormatter.convert(header.value) : `${header.value}`,
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipYAccessor = yAccessor === seriesIdentifier.yAccessor ? yAccessor : null;
|
||||
if (tooltipYAccessor) {
|
||||
const yFormatter = formatFactory(layerFormats.yAccessors[tooltipYAccessor]);
|
||||
data.push({
|
||||
label: layerTitles?.yTitles?.[tooltipYAccessor],
|
||||
value: yFormatter ? yFormatter.convert(pickedValue.value) : `${pickedValue.value}`,
|
||||
});
|
||||
}
|
||||
seriesIdentifier.splitAccessors.forEach((splitValue, key) => {
|
||||
const splitSeriesFormatter = formattedColumns[key]
|
||||
? null
|
||||
: formatFactory(layerFormats.splitSeriesAccessors[key]);
|
||||
|
||||
const label = layerTitles?.splitSeriesTitles?.[key];
|
||||
const value = splitSeriesFormatter ? splitSeriesFormatter.convert(splitValue) : `${splitValue}`;
|
||||
data.push({ label, value });
|
||||
});
|
||||
|
||||
if (
|
||||
splitAccessors?.splitColumnAccessor &&
|
||||
seriesIdentifier.smVerticalAccessorValue !== undefined
|
||||
) {
|
||||
data.push({
|
||||
label: layerTitles?.splitColumnTitles?.[splitAccessors?.splitColumnAccessor],
|
||||
value: `${seriesIdentifier.smVerticalAccessorValue}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
splitAccessors?.splitRowAccessor &&
|
||||
seriesIdentifier.smHorizontalAccessorValue !== undefined
|
||||
) {
|
||||
data.push({
|
||||
label: layerTitles?.splitRowTitles?.[splitAccessors?.splitRowAccessor],
|
||||
value: `${seriesIdentifier.smHorizontalAccessorValue}`,
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipRows = data.map((tooltipRow, index) => (
|
||||
<TooltipRow {...tooltipRow} key={`${tooltipRow.label}-${tooltipRow.value}-${index}`} />
|
||||
));
|
||||
|
||||
const renderEndzoneTooltip = header ? isEndzoneBucket(header?.value, xDomain) : false;
|
||||
|
||||
return (
|
||||
<div className="detailedTooltip">
|
||||
{renderEndzoneTooltip && (
|
||||
<div className="detailedTooltip__header">
|
||||
<EndzoneTooltipHeader />
|
||||
</div>
|
||||
)}
|
||||
<table>
|
||||
<tbody>{tooltipRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TooltipHeader } from './tooltip_header';
|
||||
import { EndzoneTooltipHeader } from './endzone_tooltip_header';
|
||||
|
||||
describe('TooltipHeader', () => {
|
||||
const formatter = (value: unknown) => `formatted-${value}`;
|
||||
|
||||
const xDomain = { min: 10, max: 100 };
|
||||
|
||||
it('should handle endzone bucket', () => {
|
||||
const value = 1;
|
||||
const expectedValue = formatter(value);
|
||||
const tooltipHeader = shallow(
|
||||
<TooltipHeader xDomain={xDomain} formatter={formatter} value={value} />
|
||||
);
|
||||
|
||||
const endzoneTooltip = tooltipHeader.find(EndzoneTooltipHeader);
|
||||
expect(endzoneTooltip.exists()).toBeTruthy();
|
||||
expect(endzoneTooltip.prop('value')).toEqual(expectedValue);
|
||||
|
||||
const minInterval = 99.5;
|
||||
const newValue = 11;
|
||||
const newExpectedValue = formatter(newValue);
|
||||
|
||||
const tooltipHeaderWithMinInterval = shallow(
|
||||
<TooltipHeader xDomain={{ ...xDomain, minInterval }} formatter={formatter} value={newValue} />
|
||||
);
|
||||
|
||||
const endzoneTooltipWithMinInterval = tooltipHeaderWithMinInterval.find(EndzoneTooltipHeader);
|
||||
expect(endzoneTooltipWithMinInterval.exists()).toBeTruthy();
|
||||
expect(endzoneTooltipWithMinInterval.prop('value')).toEqual(newExpectedValue);
|
||||
});
|
||||
|
||||
it('should render plain value at the header', () => {
|
||||
const value = 15;
|
||||
const tooltipHeader = shallow(
|
||||
<TooltipHeader xDomain={xDomain} formatter={formatter} value={value} />
|
||||
);
|
||||
|
||||
expect(tooltipHeader).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { XDomain } from '../x_domain';
|
||||
import { EndzoneTooltipHeader } from './endzone_tooltip_header';
|
||||
import { isEndzoneBucket } from './utils';
|
||||
|
||||
interface Props {
|
||||
value: unknown;
|
||||
formatter: (value: unknown) => string;
|
||||
xDomain?: XDomain;
|
||||
}
|
||||
|
||||
export const TooltipHeader: FC<Props> = ({ value, formatter, xDomain }) => {
|
||||
const renderEndzoneHeader =
|
||||
xDomain && typeof value === 'number' ? isEndzoneBucket(value, xDomain) : undefined;
|
||||
|
||||
if (renderEndzoneHeader) {
|
||||
return <EndzoneTooltipHeader value={formatter(value)} />;
|
||||
}
|
||||
|
||||
return <>{formatter(value)}</>;
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TooltipRow } from './tooltip_row';
|
||||
|
||||
describe('TooltipRow', () => {
|
||||
it('should render label and value if both are specified', () => {
|
||||
const tooltipRow = shallow(<TooltipRow value={'0'} label={'tooltip'} />);
|
||||
expect(tooltipRow).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return null if either label or value is not specified', () => {
|
||||
const tooltipRow1 = shallow(<TooltipRow label={'tooltip'} />);
|
||||
expect(tooltipRow1).toEqual({});
|
||||
|
||||
const tooltipRow2 = shallow(<TooltipRow value={'some value'} />);
|
||||
expect(tooltipRow2).toEqual({});
|
||||
|
||||
const tooltipRow3 = shallow(<TooltipRow />);
|
||||
expect(tooltipRow3).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
|
||||
export interface TooltipData {
|
||||
label?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const TooltipRow: FC<TooltipData> = ({ label, value }) => {
|
||||
return label && value ? (
|
||||
<tr>
|
||||
<td className="detailedTooltip__label">
|
||||
<div className="detailedTooltip__labelContainer">{label}</div>
|
||||
</td>
|
||||
|
||||
<td className="detailedTooltip__value">
|
||||
<div className="detailedTooltip__valueContainer">{value}</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : null;
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { XDomain } from '../x_domain';
|
||||
|
||||
export const isEndzoneBucket = (
|
||||
value: number,
|
||||
{ min, max, minInterval }: XDomain | undefined = {}
|
||||
) => {
|
||||
return (
|
||||
(min !== undefined && min > value) ||
|
||||
(max !== undefined && minInterval !== undefined && max - minInterval < value)
|
||||
);
|
||||
};
|
|
@ -1809,8 +1809,7 @@ describe('XYChart component', () => {
|
|||
.find(LineSeries)
|
||||
.prop('name') as SeriesNameFn;
|
||||
|
||||
// In this case, the ID is used as the name. This shouldn't happen in practice
|
||||
expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(null);
|
||||
expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('a');
|
||||
expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(null);
|
||||
});
|
||||
|
||||
|
@ -1890,7 +1889,7 @@ describe('XYChart component', () => {
|
|||
// This accessor has a human-readable name
|
||||
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A');
|
||||
// This accessor does not
|
||||
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(null);
|
||||
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual('b');
|
||||
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(null);
|
||||
});
|
||||
|
||||
|
@ -2953,4 +2952,50 @@ describe('XYChart component', () => {
|
|||
expect(smallMultiples.prop('splitHorizontally')).toEqual(SPLIT_ROW);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detailed tooltip', () => {
|
||||
it('should render custom detailed tooltip', () => {
|
||||
const { args } = sampleArgs();
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
args={{
|
||||
...args,
|
||||
layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'bar' }],
|
||||
detailedTooltip: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const settings = component.find(Settings);
|
||||
const tooltip = settings.prop('tooltip');
|
||||
expect(tooltip).toEqual(
|
||||
expect.objectContaining({
|
||||
headerFormatter: undefined,
|
||||
customTooltip: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should render default tooltip, if detailed tooltip is hidden', () => {
|
||||
const { args } = sampleArgs();
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
args={{
|
||||
...args,
|
||||
layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'bar' }],
|
||||
detailedTooltip: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const settings = component.find(Settings);
|
||||
const tooltip = settings.prop('tooltip');
|
||||
expect(tooltip).toEqual(
|
||||
expect.objectContaining({
|
||||
headerFormatter: expect.any(Function),
|
||||
customTooltip: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -57,12 +57,10 @@ import {
|
|||
getAnnotationsLayers,
|
||||
getDataLayers,
|
||||
Series,
|
||||
getFormat,
|
||||
isReferenceLineYConfig,
|
||||
getFormattedTablesByLayers,
|
||||
} from '../helpers';
|
||||
|
||||
import {
|
||||
getLayersFormats,
|
||||
getLayersTitles,
|
||||
isReferenceLineYConfig,
|
||||
getFilteredLayers,
|
||||
getReferenceLayers,
|
||||
isDataLayer,
|
||||
|
@ -86,9 +84,11 @@ import {
|
|||
} from './annotations';
|
||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants';
|
||||
import { DataLayers } from './data_layers';
|
||||
import { Tooltip } from './tooltip';
|
||||
import { XYCurrentTime } from './xy_current_time';
|
||||
|
||||
import './xy_chart.scss';
|
||||
import { TooltipHeader } from './tooltip';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -195,6 +195,11 @@ export function XYChart({
|
|||
[dataLayers, formatFactory]
|
||||
);
|
||||
|
||||
const fieldFormats = useMemo(
|
||||
() => getLayersFormats(dataLayers, { splitColumnAccessor, splitRowAccessor }),
|
||||
[dataLayers, splitColumnAccessor, splitRowAccessor]
|
||||
);
|
||||
|
||||
if (dataLayers.length === 0) {
|
||||
const icon: IconType = getIconForSeriesType(
|
||||
getDataLayers(layers)?.[0]?.seriesType || SeriesTypes.BAR
|
||||
|
@ -208,9 +213,7 @@ export function XYChart({
|
|||
: undefined;
|
||||
|
||||
const xAxisFormatter = formatFactory(
|
||||
dataLayers[0].xAccessor
|
||||
? getFormat(dataLayers[0].table.columns, dataLayers[0].xAccessor)
|
||||
: undefined
|
||||
xAxisColumn?.id ? fieldFormats[dataLayers[0].layerId].xAccessors[xAxisColumn?.id] : undefined
|
||||
);
|
||||
|
||||
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
|
||||
|
@ -229,11 +232,24 @@ export function XYChart({
|
|||
dataLayers,
|
||||
shouldRotate,
|
||||
formatFactory,
|
||||
fieldFormats,
|
||||
yLeftScale,
|
||||
yRightScale
|
||||
);
|
||||
|
||||
const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name);
|
||||
const yAxesMap = {
|
||||
left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'),
|
||||
right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
|
||||
};
|
||||
|
||||
const titles = getLayersTitles(
|
||||
dataLayers,
|
||||
{ splitColumnAccessor, splitRowAccessor },
|
||||
{ xTitle: args.xTitle, yTitle: args.yTitle, yRightTitle: args.yRightTitle },
|
||||
yAxesConfiguration
|
||||
);
|
||||
|
||||
const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
|
@ -267,24 +283,10 @@ export function XYChart({
|
|||
isHistogramViz
|
||||
);
|
||||
|
||||
const yAxesMap = {
|
||||
left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'),
|
||||
right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
|
||||
};
|
||||
|
||||
const getYAxesTitles = (axisSeries: Series[], groupId: 'right' | 'left') => {
|
||||
const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle;
|
||||
return (
|
||||
yTitle ||
|
||||
axisSeries
|
||||
.map(
|
||||
(series) =>
|
||||
filteredLayers
|
||||
.find(({ layerId }) => series.layer === layerId)
|
||||
?.table.columns.find((column) => column.id === series.accessor)?.name
|
||||
)
|
||||
.filter((name) => Boolean(name))[0]
|
||||
);
|
||||
const getYAxesTitles = (axisSeries: Series[]) => {
|
||||
return axisSeries
|
||||
.map(({ layer, accessor }) => titles?.[layer]?.yTitles?.[accessor])
|
||||
.filter((name) => Boolean(name))[0];
|
||||
};
|
||||
|
||||
const referenceLineLayers = getReferenceLayers(layers);
|
||||
|
@ -428,9 +430,11 @@ export function XYChart({
|
|||
const xAccessor = layer.xAccessor
|
||||
? getAccessorByDimension(layer.xAccessor, table.columns)
|
||||
: undefined;
|
||||
|
||||
const xFormat = xColumn ? fieldFormats[layer.layerId].xAccessors[xColumn.id] : undefined;
|
||||
const currentXFormatter =
|
||||
xAccessor && formattedDatatables[layer.layerId]?.formattedColumns[xAccessor] && xColumn
|
||||
? formatFactory(layer.xAccessor ? getFormat(table.columns, layer.xAccessor) : undefined)
|
||||
? formatFactory(xFormat)
|
||||
: xAxisFormatter;
|
||||
|
||||
const rowIndex = table.rows.findIndex((row) => {
|
||||
|
@ -457,9 +461,10 @@ export function XYChart({
|
|||
? getAccessorByDimension(layer.splitAccessor, table.columns)
|
||||
: undefined;
|
||||
|
||||
const splitFormatter = formatFactory(
|
||||
layer.splitAccessor ? getFormat(table.columns, layer.splitAccessor) : undefined
|
||||
);
|
||||
const splitFormat = splitAccessor
|
||||
? fieldFormats[layer.layerId].splitSeriesAccessors[splitAccessor]
|
||||
: undefined;
|
||||
const splitFormatter = formatFactory(splitFormat);
|
||||
|
||||
points.push({
|
||||
row: table.rows.findIndex((row) => {
|
||||
|
@ -552,6 +557,22 @@ export function XYChart({
|
|||
};
|
||||
const isSplitChart = splitColumnAccessor || splitRowAccessor;
|
||||
const splitTable = isSplitChart ? dataLayers[0].table : undefined;
|
||||
const splitColumnId =
|
||||
splitColumnAccessor && splitTable
|
||||
? getAccessorByDimension(splitColumnAccessor, splitTable?.columns)
|
||||
: undefined;
|
||||
|
||||
const splitRowId =
|
||||
splitRowAccessor && splitTable
|
||||
? getAccessorByDimension(splitRowAccessor, splitTable?.columns)
|
||||
: undefined;
|
||||
const splitLayerFieldFormats = fieldFormats[dataLayers[0].layerId];
|
||||
const splitFieldFormats = {
|
||||
...(splitColumnId
|
||||
? { [splitColumnId]: splitLayerFieldFormats.splitColumnAccessors[splitColumnId] }
|
||||
: {}),
|
||||
...(splitRowId ? { [splitRowId]: splitLayerFieldFormats.splitRowAccessors[splitRowId] } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Chart ref={chartRef}>
|
||||
|
@ -596,7 +617,32 @@ export function XYChart({
|
|||
baseTheme={chartBaseTheme}
|
||||
tooltip={{
|
||||
boundary: document.getElementById('app-fixed-viewport') ?? undefined,
|
||||
headerFormatter: (d) => safeXAccessorLabelRenderer(d.value),
|
||||
headerFormatter: !args.detailedTooltip
|
||||
? ({ value }) => (
|
||||
<TooltipHeader
|
||||
value={value}
|
||||
formatter={safeXAccessorLabelRenderer}
|
||||
xDomain={rawXDomain}
|
||||
/>
|
||||
)
|
||||
: undefined,
|
||||
customTooltip: args.detailedTooltip
|
||||
? ({ header, values }) => (
|
||||
<Tooltip
|
||||
header={header}
|
||||
values={values}
|
||||
titles={titles}
|
||||
fieldFormats={fieldFormats}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={formattedDatatables}
|
||||
splitAccessors={{
|
||||
splitColumnAccessor: splitColumnId,
|
||||
splitRowAccessor: splitRowId,
|
||||
}}
|
||||
xDomain={isTimeViz ? rawXDomain : undefined}
|
||||
/>
|
||||
)
|
||||
: undefined,
|
||||
type: args.showTooltip ? TooltipType.VerticalCursor : TooltipType.None,
|
||||
}}
|
||||
allowBrushingLastHistogramBin={isTimeViz}
|
||||
|
@ -642,6 +688,7 @@ export function XYChart({
|
|||
splitRowAccessor={splitRowAccessor}
|
||||
formatFactory={formatFactory}
|
||||
columns={splitTable.columns}
|
||||
fieldFormats={splitFieldFormats}
|
||||
/>
|
||||
)}
|
||||
{yAxesConfiguration.map((axis) => {
|
||||
|
@ -651,7 +698,7 @@ export function XYChart({
|
|||
id={axis.groupId}
|
||||
groupId={axis.groupId}
|
||||
position={axis.position}
|
||||
title={getYAxesTitles(axis.series, axis.groupId)}
|
||||
title={getYAxesTitles(axis.series)}
|
||||
gridLine={{
|
||||
visible:
|
||||
axis.groupId === 'right'
|
||||
|
@ -685,6 +732,7 @@ export function XYChart({
|
|||
|
||||
{dataLayers.length && (
|
||||
<DataLayers
|
||||
titles={titles}
|
||||
layers={dataLayers}
|
||||
endValue={endValue}
|
||||
timeZone={timeZone}
|
||||
|
@ -717,6 +765,7 @@ export function XYChart({
|
|||
}}
|
||||
isHorizontal={shouldRotate}
|
||||
paddingMap={linesPaddings}
|
||||
titles={titles}
|
||||
/>
|
||||
) : null}
|
||||
{rangeAnnotations.length || groupedLineAnnotations.length ? (
|
||||
|
|
|
@ -10,6 +10,7 @@ import { DataLayerConfig } from '../../common';
|
|||
import { LayerTypes } from '../../common/constants';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { getAxesConfiguration } from './axes_configuration';
|
||||
import { LayersFieldFormats } from './layers';
|
||||
|
||||
describe('axes_configuration', () => {
|
||||
const tables: Record<string, Datatable> = {
|
||||
|
@ -236,9 +237,23 @@ describe('axes_configuration', () => {
|
|||
table: tables.first,
|
||||
};
|
||||
|
||||
const fieldFormats: LayersFieldFormats = {
|
||||
first: {
|
||||
xAccessors: { c: { id: 'number', params: {} } },
|
||||
yAccessors: {
|
||||
yAccessorId: { id: 'number', params: {} },
|
||||
yAccessorId3: { id: 'currency', params: {} },
|
||||
yAccessorId4: { id: 'currency', params: {} },
|
||||
},
|
||||
splitSeriesAccessors: { d: { id: 'number', params: {} } },
|
||||
splitColumnAccessors: {},
|
||||
splitRowAccessors: {},
|
||||
},
|
||||
};
|
||||
|
||||
it('should map auto series to left axis', () => {
|
||||
const formatFactory = jest.fn();
|
||||
const groups = getAxesConfiguration([sampleLayer], false, formatFactory);
|
||||
const groups = getAxesConfiguration([sampleLayer], false, formatFactory, fieldFormats);
|
||||
expect(groups.length).toEqual(1);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[0].series[0].accessor).toEqual('yAccessorId');
|
||||
|
@ -248,7 +263,7 @@ describe('axes_configuration', () => {
|
|||
it('should map auto series to right axis if formatters do not match', () => {
|
||||
const formatFactory = jest.fn();
|
||||
const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] };
|
||||
const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory);
|
||||
const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory, fieldFormats);
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[1].position).toEqual('right');
|
||||
|
@ -262,7 +277,7 @@ describe('axes_configuration', () => {
|
|||
...sampleLayer,
|
||||
accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'],
|
||||
};
|
||||
const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory);
|
||||
const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory, fieldFormats);
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
expect(groups[1].position).toEqual('right');
|
||||
|
@ -281,7 +296,8 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
formatFactory
|
||||
formatFactory,
|
||||
fieldFormats
|
||||
);
|
||||
expect(groups.length).toEqual(1);
|
||||
expect(groups[0].position).toEqual('right');
|
||||
|
@ -300,7 +316,8 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
formatFactory
|
||||
formatFactory,
|
||||
fieldFormats
|
||||
);
|
||||
expect(groups.length).toEqual(2);
|
||||
expect(groups[0].position).toEqual('left');
|
||||
|
@ -308,8 +325,8 @@ describe('axes_configuration', () => {
|
|||
expect(groups[0].series[1].accessor).toEqual('yAccessorId4');
|
||||
expect(groups[1].position).toEqual('right');
|
||||
expect(groups[1].series[0].accessor).toEqual('yAccessorId');
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'number' });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'number', params: {} });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency', params: {} });
|
||||
});
|
||||
|
||||
it('should create one formatter per series group', () => {
|
||||
|
@ -323,10 +340,11 @@ describe('axes_configuration', () => {
|
|||
},
|
||||
],
|
||||
false,
|
||||
formatFactory
|
||||
formatFactory,
|
||||
fieldFormats
|
||||
);
|
||||
expect(formatFactory).toHaveBeenCalledTimes(2);
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'number' });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'number', params: {} });
|
||||
expect(formatFactory).toHaveBeenCalledWith({ id: 'currency', params: {} });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,8 +16,7 @@ import {
|
|||
YConfig,
|
||||
YScaleType,
|
||||
} from '../../common';
|
||||
import { isDataLayer } from './visualization';
|
||||
import { getFormat } from './format';
|
||||
import { LayersFieldFormats } from './layers';
|
||||
|
||||
export interface Series {
|
||||
layer: string;
|
||||
|
@ -40,10 +39,13 @@ export function isFormatterCompatible(
|
|||
formatter1: SerializedFieldFormat,
|
||||
formatter2: SerializedFieldFormat
|
||||
) {
|
||||
return formatter1.id === formatter2.id;
|
||||
return formatter1?.id === formatter2?.id;
|
||||
}
|
||||
|
||||
export function groupAxesByType(layers: CommonXYDataLayerConfig[]) {
|
||||
export function groupAxesByType(
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
fieldFormats: LayersFieldFormats
|
||||
) {
|
||||
const series: {
|
||||
auto: FormattedMetric[];
|
||||
left: FormattedMetric[];
|
||||
|
@ -57,32 +59,14 @@ export function groupAxesByType(layers: CommonXYDataLayerConfig[]) {
|
|||
};
|
||||
|
||||
layers.forEach((layer) => {
|
||||
const { table } = layer;
|
||||
const { layerId, table } = layer;
|
||||
layer.accessors.forEach((accessor) => {
|
||||
const yConfig: Array<YConfig | ExtendedYConfig> | undefined = layer.yConfig;
|
||||
const yAccessor = getAccessorByDimension(accessor, table?.columns || []);
|
||||
const yAccessor = getAccessorByDimension(accessor, table.columns);
|
||||
const mode =
|
||||
yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === yAccessor)?.axisMode || 'auto';
|
||||
let formatter: SerializedFieldFormat = getFormat(table.columns, accessor) || {
|
||||
id: 'number',
|
||||
};
|
||||
if (
|
||||
isDataLayer(layer) &&
|
||||
layer.seriesType.includes('percentage') &&
|
||||
formatter.id !== 'percent'
|
||||
) {
|
||||
formatter = {
|
||||
id: 'percent',
|
||||
params: {
|
||||
pattern: '0.[00]%',
|
||||
},
|
||||
};
|
||||
}
|
||||
series[mode].push({
|
||||
layer: layer.layerId,
|
||||
accessor: yAccessor,
|
||||
fieldFormat: formatter,
|
||||
});
|
||||
yConfig?.find(({ forAccessor }) => forAccessor === yAccessor)?.axisMode || 'auto';
|
||||
const fieldFormat = fieldFormats[layerId].yAccessors[yAccessor]!;
|
||||
series[mode].push({ layer: layer.layerId, accessor: yAccessor, fieldFormat });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,11 +101,12 @@ export function groupAxesByType(layers: CommonXYDataLayerConfig[]) {
|
|||
export function getAxesConfiguration(
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
shouldRotate: boolean,
|
||||
formatFactory?: FormatFactory,
|
||||
formatFactory: FormatFactory | undefined,
|
||||
fieldFormats: LayersFieldFormats,
|
||||
yLeftScale?: YScaleType,
|
||||
yRightScale?: YScaleType
|
||||
): GroupsConfiguration {
|
||||
const series = groupAxesByType(layers);
|
||||
const series = groupAxesByType(layers, fieldFormats);
|
||||
|
||||
const axisGroups: GroupsConfiguration = [];
|
||||
|
||||
|
|
|
@ -36,12 +36,14 @@ import { FormatFactory } from '../types';
|
|||
import { getSeriesColor } from './state';
|
||||
import { ColorAssignments } from './color_assignment';
|
||||
import { GroupsConfiguration } from './axes_configuration';
|
||||
import { LayerAccessorsTitles } from './layers';
|
||||
import { getFormat } from './format';
|
||||
|
||||
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
|
||||
|
||||
type GetSeriesPropsFn = (config: {
|
||||
layer: CommonXYDataLayerConfig;
|
||||
titles?: LayerAccessorsTitles;
|
||||
accessor: string;
|
||||
chartHasMoreThanOneBarSeries?: boolean;
|
||||
formatFactory: FormatFactory;
|
||||
|
@ -66,7 +68,8 @@ type GetSeriesNameFn = (
|
|||
splitFormatter: FieldFormat;
|
||||
alreadyFormattedColumns: Record<string, boolean>;
|
||||
columnToLabelMap: Record<string, string>;
|
||||
}
|
||||
},
|
||||
titles: LayerAccessorsTitles
|
||||
) => SeriesName;
|
||||
|
||||
type GetColorFn = (
|
||||
|
@ -78,7 +81,8 @@ type GetColorFn = (
|
|||
columnToLabelMap: Record<string, string>;
|
||||
paletteService: PaletteRegistry;
|
||||
syncColors?: boolean;
|
||||
}
|
||||
},
|
||||
titles: LayerAccessorsTitles
|
||||
) => string | null;
|
||||
|
||||
type GetPointConfigFn = (config: {
|
||||
|
@ -209,7 +213,8 @@ const getSeriesName: GetSeriesNameFn = (
|
|||
splitFormatter,
|
||||
alreadyFormattedColumns,
|
||||
columnToLabelMap,
|
||||
}
|
||||
},
|
||||
titles
|
||||
) => {
|
||||
// For multiple y series, the name of the operation is used on each, either:
|
||||
// * Key - Y name
|
||||
|
@ -221,9 +226,15 @@ const getSeriesName: GetSeriesNameFn = (
|
|||
if (i === 0 && splitHint && splitColumnId && !formatted) {
|
||||
return splitFormatter.convert(key);
|
||||
}
|
||||
return splitColumnId && i === 0 ? key : columnToLabelMap[key] ?? null;
|
||||
return splitColumnId && i === 0
|
||||
? key
|
||||
: columnToLabelMap[key] ??
|
||||
titles?.yTitles?.[key] ??
|
||||
titles?.splitSeriesTitles?.[key] ??
|
||||
null;
|
||||
})
|
||||
.join(' - ');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -235,10 +246,13 @@ const getSeriesName: GetSeriesNameFn = (
|
|||
}
|
||||
return splitFormatter.convert(data.seriesKeys[0]);
|
||||
}
|
||||
|
||||
// This handles both split and single-y cases:
|
||||
// * If split series without formatting, show the value literally
|
||||
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
|
||||
return splitColumnId ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null;
|
||||
return splitColumnId
|
||||
? data.seriesKeys[0]
|
||||
: columnToLabelMap[data.seriesKeys[0]] ?? titles?.yTitles?.[data.seriesKeys[0]] ?? null;
|
||||
};
|
||||
|
||||
const getPointConfig: GetPointConfigFn = ({
|
||||
|
@ -267,16 +281,20 @@ const getLineConfig: GetLineConfigFn = ({ showLines, lineWidth }) => ({
|
|||
|
||||
const getColor: GetColorFn = (
|
||||
{ yAccessor, seriesKeys },
|
||||
{ layer, accessor, colorAssignments, columnToLabelMap, paletteService, syncColors }
|
||||
{ layer, accessor, colorAssignments, columnToLabelMap, paletteService, syncColors },
|
||||
titles
|
||||
) => {
|
||||
const overwriteColor = getSeriesColor(layer, accessor);
|
||||
if (overwriteColor !== null) {
|
||||
return overwriteColor;
|
||||
}
|
||||
const colorAssignment = colorAssignments[layer.palette.name];
|
||||
|
||||
const seriesLayers: SeriesLayer[] = [
|
||||
{
|
||||
name: layer.splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
|
||||
name: layer.splitAccessor
|
||||
? String(seriesKeys[0])
|
||||
: columnToLabelMap[seriesKeys[0]] ?? titles?.yTitles?.[seriesKeys[0]] ?? null,
|
||||
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
|
||||
rankAtDepth: colorAssignment.getRank(layer, String(seriesKeys[0]), String(yAccessor)),
|
||||
},
|
||||
|
@ -293,8 +311,37 @@ const getColor: GetColorFn = (
|
|||
);
|
||||
};
|
||||
|
||||
const EMPTY_ACCESSOR = '-';
|
||||
const SPLIT_CHAR = '.';
|
||||
|
||||
export const generateSeriesId = (
|
||||
{
|
||||
layerId,
|
||||
xAccessor,
|
||||
splitAccessor,
|
||||
}: Pick<CommonXYDataLayerConfig, 'layerId' | 'xAccessor' | 'splitAccessor'>,
|
||||
accessor?: string
|
||||
) =>
|
||||
[
|
||||
layerId,
|
||||
xAccessor ?? EMPTY_ACCESSOR,
|
||||
accessor ?? EMPTY_ACCESSOR,
|
||||
splitAccessor ?? EMPTY_ACCESSOR,
|
||||
].join(SPLIT_CHAR);
|
||||
|
||||
export const getMetaFromSeriesId = (seriesId: string) => {
|
||||
const [layerId, xAccessor, yAccessor, splitAccessor] = seriesId.split(SPLIT_CHAR);
|
||||
return {
|
||||
layerId,
|
||||
xAccessor: xAccessor === EMPTY_ACCESSOR ? undefined : xAccessor,
|
||||
yAccessor,
|
||||
splitAccessor: splitAccessor === EMPTY_ACCESSOR ? undefined : splitAccessor,
|
||||
};
|
||||
};
|
||||
|
||||
export const getSeriesProps: GetSeriesPropsFn = ({
|
||||
layer,
|
||||
titles = {},
|
||||
accessor,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
colorAssignments,
|
||||
|
@ -363,8 +410,8 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
|
||||
return {
|
||||
splitSeriesAccessors: splitColumnId ? [splitColumnId] : [],
|
||||
stackAccessors: isStacked ? [xColumnId as string] : [],
|
||||
id: splitColumnId ? `${splitColumnId}-${accessor}` : accessor,
|
||||
stackAccessors: isStacked ? [layer.xAccessor as string] : [],
|
||||
id: generateSeriesId(layer, accessor),
|
||||
xAccessor: xColumnId || 'unifiedX',
|
||||
yAccessors: [accessor],
|
||||
markSizeAccessor: markSizeColumnId,
|
||||
|
@ -376,14 +423,18 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
? ScaleType.LinearBinary
|
||||
: yAxis?.scale || ScaleType.Linear,
|
||||
color: (series) =>
|
||||
getColor(series, {
|
||||
layer,
|
||||
accessor,
|
||||
colorAssignments,
|
||||
columnToLabelMap,
|
||||
paletteService,
|
||||
syncColors,
|
||||
}),
|
||||
getColor(
|
||||
series,
|
||||
{
|
||||
layer,
|
||||
accessor,
|
||||
colorAssignments,
|
||||
columnToLabelMap,
|
||||
paletteService,
|
||||
syncColors,
|
||||
},
|
||||
titles
|
||||
),
|
||||
groupId: yAxis?.groupId,
|
||||
enableHistogramMode,
|
||||
stackMode: isPercentage ? StackMode.Percentage : undefined,
|
||||
|
@ -417,14 +468,18 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
line: getLineConfig({ lineWidth: layer.lineWidth, showLines: layer.showLines }),
|
||||
},
|
||||
name(d) {
|
||||
return getSeriesName(d, {
|
||||
splitColumnId,
|
||||
accessorsCount: layer.accessors.length,
|
||||
splitHint,
|
||||
splitFormatter,
|
||||
alreadyFormattedColumns: formattedColumns,
|
||||
columnToLabelMap,
|
||||
});
|
||||
return getSeriesName(
|
||||
d,
|
||||
{
|
||||
splitColumnId,
|
||||
accessorsCount: layer.accessors.length,
|
||||
splitHint,
|
||||
splitFormatter,
|
||||
alreadyFormattedColumns: formattedColumns,
|
||||
columnToLabelMap,
|
||||
},
|
||||
titles
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,15 +7,57 @@
|
|||
*/
|
||||
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils';
|
||||
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
|
||||
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
|
||||
import {
|
||||
getAccessorByDimension,
|
||||
getColumnByAccessor,
|
||||
} from '@kbn/visualizations-plugin/common/utils';
|
||||
import {
|
||||
CommonXYDataLayerConfig,
|
||||
CommonXYLayerConfig,
|
||||
ReferenceLineLayerConfig,
|
||||
SeriesType,
|
||||
} from '../../common/types';
|
||||
import { GroupsConfiguration } from './axes_configuration';
|
||||
import { getFormat } from './format';
|
||||
import { isDataLayer, isReferenceLayer } from './visualization';
|
||||
|
||||
interface CustomTitles {
|
||||
xTitle?: string;
|
||||
yTitle?: string;
|
||||
yRightTitle?: string;
|
||||
}
|
||||
|
||||
interface SplitAccessors {
|
||||
splitColumnAccessor?: string | ExpressionValueVisDimension;
|
||||
splitRowAccessor?: string | ExpressionValueVisDimension;
|
||||
}
|
||||
|
||||
export type AccessorsFieldFormats = Record<string, SerializedFieldFormat | undefined>;
|
||||
|
||||
export interface LayerFieldFormats {
|
||||
xAccessors: AccessorsFieldFormats;
|
||||
yAccessors: AccessorsFieldFormats;
|
||||
splitSeriesAccessors: AccessorsFieldFormats;
|
||||
splitColumnAccessors: AccessorsFieldFormats;
|
||||
splitRowAccessors: AccessorsFieldFormats;
|
||||
}
|
||||
|
||||
export type LayersFieldFormats = Record<string, LayerFieldFormats>;
|
||||
|
||||
export type AccessorsTitles = Record<string, string>;
|
||||
|
||||
export interface LayerAccessorsTitles {
|
||||
xTitles?: AccessorsTitles;
|
||||
yTitles?: AccessorsTitles;
|
||||
splitSeriesTitles?: AccessorsTitles;
|
||||
splitColumnTitles?: AccessorsTitles;
|
||||
splitRowTitles?: AccessorsTitles;
|
||||
}
|
||||
|
||||
export type LayersAccessorsTitles = Record<string, LayerAccessorsTitles>;
|
||||
|
||||
export function getFilteredLayers(layers: CommonXYLayerConfig[]) {
|
||||
return layers.filter<ReferenceLineLayerConfig | CommonXYDataLayerConfig>(
|
||||
(layer): layer is ReferenceLineLayerConfig | CommonXYDataLayerConfig => {
|
||||
|
@ -52,3 +94,140 @@ export function getFilteredLayers(layers: CommonXYLayerConfig[]) {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
const getAccessorWithFieldFormat = (
|
||||
dimension: string | ExpressionValueVisDimension | undefined,
|
||||
columns: Datatable['columns']
|
||||
) => {
|
||||
if (!dimension) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const accessor = getAccessorByDimension(dimension, columns);
|
||||
return { [accessor]: getFormat(columns, dimension) };
|
||||
};
|
||||
|
||||
const getYAccessorWithFieldFormat = (
|
||||
dimension: string | ExpressionValueVisDimension | undefined,
|
||||
columns: Datatable['columns'],
|
||||
seriesType: SeriesType
|
||||
) => {
|
||||
if (!dimension) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const accessor = getAccessorByDimension(dimension, columns);
|
||||
let format = getFormat(columns, dimension) ?? { id: 'number' };
|
||||
if (format?.id !== 'percent' && seriesType.includes('percentage')) {
|
||||
format = { id: 'percent', params: { pattern: '0.[00]%' } };
|
||||
}
|
||||
|
||||
return { [accessor]: format };
|
||||
};
|
||||
|
||||
export const getLayerFormats = (
|
||||
{ xAccessor, accessors, splitAccessor, table, seriesType }: CommonXYDataLayerConfig,
|
||||
{ splitColumnAccessor, splitRowAccessor }: SplitAccessors
|
||||
): LayerFieldFormats => {
|
||||
const yAccessors: Array<string | ExpressionValueVisDimension> = accessors;
|
||||
return {
|
||||
xAccessors: getAccessorWithFieldFormat(xAccessor, table.columns),
|
||||
yAccessors: yAccessors.reduce(
|
||||
(formatters, a) => ({
|
||||
...formatters,
|
||||
...getYAccessorWithFieldFormat(a, table.columns, seriesType),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
splitSeriesAccessors: getAccessorWithFieldFormat(splitAccessor, table.columns),
|
||||
splitColumnAccessors: getAccessorWithFieldFormat(splitColumnAccessor, table.columns),
|
||||
splitRowAccessors: getAccessorWithFieldFormat(splitRowAccessor, table.columns),
|
||||
};
|
||||
};
|
||||
|
||||
export const getLayersFormats = (
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
splitAccessors: SplitAccessors
|
||||
): LayersFieldFormats =>
|
||||
layers.reduce<LayersFieldFormats>(
|
||||
(formatters, layer) => ({
|
||||
...formatters,
|
||||
[layer.layerId]: getLayerFormats(layer, splitAccessors),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const getTitleForYAccessor = (
|
||||
layerId: string,
|
||||
yAccessor: string | ExpressionValueVisDimension,
|
||||
{ yTitle, yRightTitle }: Omit<CustomTitles, 'xTitle'>,
|
||||
groups: GroupsConfiguration,
|
||||
columns: Datatable['columns']
|
||||
) => {
|
||||
const column = getColumnByAccessor(yAccessor, columns);
|
||||
const isRight = groups.some((group) =>
|
||||
group.series.some(
|
||||
({ accessor, layer }) =>
|
||||
accessor === yAccessor && layer === layerId && group.groupId === 'right'
|
||||
)
|
||||
);
|
||||
if (isRight) {
|
||||
return yRightTitle || column!.name;
|
||||
}
|
||||
|
||||
return yTitle || column!.name;
|
||||
};
|
||||
|
||||
export const getLayerTitles = (
|
||||
{ xAccessor, accessors, splitAccessor, table, layerId }: CommonXYDataLayerConfig,
|
||||
{ splitColumnAccessor, splitRowAccessor }: SplitAccessors,
|
||||
{ xTitle, yTitle, yRightTitle }: CustomTitles,
|
||||
groups: GroupsConfiguration
|
||||
): LayerAccessorsTitles => {
|
||||
const mapTitle = (dimension?: string | ExpressionValueVisDimension) => {
|
||||
if (!dimension) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const column = getColumnByAccessor(dimension, table.columns);
|
||||
return { [column!.id]: column!.name };
|
||||
};
|
||||
|
||||
const getYTitle = (accessor: string) => ({
|
||||
[accessor]: getTitleForYAccessor(
|
||||
layerId,
|
||||
accessor,
|
||||
{ yTitle, yRightTitle },
|
||||
groups,
|
||||
table.columns
|
||||
),
|
||||
});
|
||||
|
||||
const xColumnId = xAccessor && getAccessorByDimension(xAccessor, table.columns);
|
||||
const yColumnIds = accessors.map((a) => a && getAccessorByDimension(a, table.columns));
|
||||
|
||||
return {
|
||||
xTitles: xTitle && xColumnId ? { [xColumnId]: xTitle } : mapTitle(xColumnId),
|
||||
yTitles: yColumnIds.reduce(
|
||||
(titles, yAccessor) => ({ ...titles, ...(yAccessor ? getYTitle(yAccessor) : {}) }),
|
||||
{}
|
||||
),
|
||||
splitSeriesTitles: mapTitle(splitAccessor),
|
||||
splitColumnTitles: mapTitle(splitColumnAccessor),
|
||||
splitRowTitles: mapTitle(splitRowAccessor),
|
||||
};
|
||||
};
|
||||
|
||||
export const getLayersTitles = (
|
||||
layers: CommonXYDataLayerConfig[],
|
||||
splitAccessors: SplitAccessors,
|
||||
customTitles: CustomTitles,
|
||||
groups: GroupsConfiguration
|
||||
): LayersAccessorsTitles =>
|
||||
layers.reduce<LayersAccessorsTitles>(
|
||||
(formatters, layer) => ({
|
||||
...formatters,
|
||||
[layer.layerId]: getLayerTitles(layer, splitAccessors, customTitles, groups),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
|
|
@ -3437,6 +3437,7 @@
|
|||
"expressionTagcloud.functions.tagcloudHelpText": "Visualisation du nuage de balises.",
|
||||
"expressionTagcloud.renderer.tagcloud.displayName": "Visualisation du nuage de balises",
|
||||
"expressionTagcloud.renderer.tagcloud.helpDescription": "Afficher le rendu d’un nuage de balises",
|
||||
"expressionXY.partialData.bucketTooltipText": "La plage temporelle sélectionnée n'inclut pas ce compartiment en entier. Il se peut qu'elle contienne des données partielles.",
|
||||
"expressionXY.axisExtentConfig.extentMode.help": "Mode d'extension",
|
||||
"expressionXY.axisExtentConfig.help": "Configurer les étendues d’axe du graphique xy",
|
||||
"expressionXY.axisExtentConfig.lowerBound.help": "Limite inférieure",
|
||||
|
|
|
@ -3531,6 +3531,7 @@
|
|||
"expressionTagcloud.functions.tagcloudHelpText": "Tagcloudのビジュアライゼーションです。",
|
||||
"expressionTagcloud.renderer.tagcloud.displayName": "Tag Cloudのビジュアライゼーションです",
|
||||
"expressionTagcloud.renderer.tagcloud.helpDescription": "Tag Cloudを表示",
|
||||
"expressionXY.partialData.bucketTooltipText": "選択された時間範囲にはこのバケット全体は含まれていません。一部データが含まれている可能性があります。",
|
||||
"expressionXY.axisExtentConfig.extentMode.help": "範囲モード",
|
||||
"expressionXY.axisExtentConfig.help": "xyグラフの軸範囲を構成",
|
||||
"expressionXY.axisExtentConfig.lowerBound.help": "下界",
|
||||
|
|
|
@ -3541,6 +3541,7 @@
|
|||
"expressionTagcloud.functions.tagcloudHelpText": "标签云图可视化。",
|
||||
"expressionTagcloud.renderer.tagcloud.displayName": "标签云图可视化",
|
||||
"expressionTagcloud.renderer.tagcloud.helpDescription": "呈现标签云图",
|
||||
"expressionXY.partialData.bucketTooltipText": "选定的时间范围不包括此整个存储桶。其可能包含部分数据。",
|
||||
"expressionXY.axisExtentConfig.extentMode.help": "范围模式",
|
||||
"expressionXY.axisExtentConfig.help": "配置 xy 图表的轴范围",
|
||||
"expressionXY.axisExtentConfig.lowerBound.help": "下边界",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue