[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:
Yaroslav Kuznietsov 2022-06-01 20:26:45 +03:00 committed by GitHub
parent be3c3b52db
commit ca36c6dff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1842 additions and 110 deletions

View file

@ -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,

View file

@ -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',

View file

@ -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;
}

View file

@ -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 [

View file

@ -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,

View file

@ -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(

View file

@ -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} />;
})}
</>
);

View file

@ -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(

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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();
});
});

View file

@ -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>
</>
)}
</>
);

View file

@ -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';

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -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>
);
};

View file

@ -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();
});
});

View file

@ -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)}</>;
};

View file

@ -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({});
});
});

View file

@ -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;
};

View file

@ -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)
);
};

View file

@ -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,
})
);
});
});
});

View file

@ -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 ? (

View file

@ -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: {} });
});
});

View file

@ -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 = [];

View file

@ -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
);
},
};
};

View file

@ -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),
}),
{}
);

View file

@ -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 dun 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 daxe du graphique xy",
"expressionXY.axisExtentConfig.lowerBound.help": "Limite inférieure",

View file

@ -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": "下界",

View file

@ -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": "下边界",