kibana/x-pack/plugins/lens/public/xy_visualization/expression.tsx
Stratoula Kalafateli 8f70030386
[Lens] [TSVB] Fixes the brushing of the last bucket for timeseries visualizations (#112068)
* Enable allowBrushingLastHistogramBucket for timeseries visualizations

* Cleanup
2021-09-15 15:20:44 +03:00

861 lines
29 KiB
TypeScript

/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import './expression.scss';
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import {
Chart,
Settings,
Axis,
LineSeries,
AreaSeries,
BarSeries,
Position,
GeometryValue,
XYChartSeriesIdentifier,
StackMode,
VerticalAlignment,
HorizontalAlignment,
LayoutDirection,
ElementClickListener,
BrushEndListener,
CurveType,
LegendPositionConfig,
LabelOverflowConstraint,
} from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import type {
ExpressionRenderDefinition,
Datatable,
DatatableRow,
} from 'src/plugins/expressions/public';
import { IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RenderMode } from 'src/plugins/expressions';
import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types';
import type { LensMultiTable, FormatFactory } from '../../common';
import { layerTypes } from '../../common';
import type { LayerArgs, SeriesType, XYChartProps } from '../../common/expressions';
import { visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
import { isHorizontalChart, getSeriesColor } from './state_helpers';
import { search } from '../../../../../src/plugins/data/public';
import {
ChartsPluginSetup,
ChartsPluginStart,
PaletteRegistry,
SeriesLayer,
useActiveCursor,
} from '../../../../../src/plugins/charts/public';
import { EmptyPlaceholder } from '../shared_components';
import { getFitOptions } from './fitting_functions';
import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration';
import { getColorAssignments } from './color_assignment';
import { getXDomain, XyEndzones } from './x_domain';
import { getLegendAction } from './get_legend_action';
declare global {
interface Window {
/**
* Flag used to enable debugState on elastic charts
*/
_echDebugStateFlag?: boolean;
}
}
type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T;
type SeriesSpec = InferPropType<typeof LineSeries> &
InferPropType<typeof BarSeries> &
InferPropType<typeof AreaSeries>;
export type XYChartRenderProps = XYChartProps & {
chartsThemeService: ChartsPluginSetup['theme'];
chartsActiveCursorService: ChartsPluginStart['activeCursor'];
paletteService: PaletteRegistry;
formatFactory: FormatFactory;
timeZone: string;
minInterval: number | undefined;
interactive?: boolean;
onClickValue: (data: LensFilterEvent['data']) => void;
onSelectRange: (data: LensBrushEvent['data']) => void;
renderMode: RenderMode;
syncColors: boolean;
};
export function calculateMinInterval({ args: { layers }, data }: XYChartProps) {
const filteredLayers = getFilteredLayers(layers, data);
if (filteredLayers.length === 0) return;
const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time');
const xColumn = data.tables[filteredLayers[0].layerId].columns.find(
(column) => column.id === filteredLayers[0].xAccessor
);
if (!xColumn) return;
if (!isTimeViz) {
const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn);
if (typeof histogramInterval === 'number') {
return histogramInterval;
} else {
return undefined;
}
}
const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval;
if (!dateInterval) return;
const intervalDuration = search.aggs.parseInterval(dateInterval);
if (!intervalDuration) return;
return intervalDuration.as('milliseconds');
}
export const getXyChartRenderer = (dependencies: {
formatFactory: FormatFactory;
chartsThemeService: ChartsPluginStart['theme'];
chartsActiveCursorService: ChartsPluginStart['activeCursor'];
paletteService: PaletteRegistry;
timeZone: string;
}): ExpressionRenderDefinition<XYChartProps> => ({
name: 'lens_xy_chart_renderer',
displayName: 'XY chart',
help: i18n.translate('xpack.lens.xyChart.renderer.help', {
defaultMessage: 'X/Y chart renderer',
}),
validate: () => undefined,
reuseDomNode: true,
render: async (
domNode: Element,
config: XYChartProps,
handlers: ILensInterpreterRenderHandlers
) => {
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
const onClickValue = (data: LensFilterEvent['data']) => {
handlers.event({ name: 'filter', data });
};
const onSelectRange = (data: LensBrushEvent['data']) => {
handlers.event({ name: 'brush', data });
};
ReactDOM.render(
<I18nProvider>
<XYChartReportable
{...config}
formatFactory={dependencies.formatFactory}
chartsActiveCursorService={dependencies.chartsActiveCursorService}
chartsThemeService={dependencies.chartsThemeService}
paletteService={dependencies.paletteService}
timeZone={dependencies.timeZone}
minInterval={calculateMinInterval(config)}
interactive={handlers.isInteractive()}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
renderMode={handlers.getRenderMode()}
syncColors={handlers.isSyncColorsEnabled()}
/>
</I18nProvider>,
domNode,
() => handlers.done()
);
},
});
function getValueLabelsStyling(isHorizontal: boolean) {
const VALUE_LABELS_MAX_FONTSIZE = 12;
const VALUE_LABELS_MIN_FONTSIZE = 10;
const VALUE_LABELS_VERTICAL_OFFSET = -10;
const VALUE_LABELS_HORIZONTAL_OFFSET = 10;
return {
displayValue: {
fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE },
fill: { textContrast: true, textInverted: false, textBorder: 0 },
alignment: isHorizontal
? {
vertical: VerticalAlignment.Middle,
}
: { horizontal: HorizontalAlignment.Center },
offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0,
offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET,
},
};
}
function getIconForSeriesType(seriesType: SeriesType): IconType {
return visualizationTypes.find((c) => c.id === seriesType)!.icon || 'empty';
}
const MemoizedChart = React.memo(XYChart);
export function XYChartReportable(props: XYChartRenderProps) {
const [state, setState] = useState({
isReady: false,
});
// It takes a cycle for the XY chart to render. This prevents
// reporting from printing a blank chart placeholder.
useEffect(() => {
setState({ isReady: true });
}, [setState]);
return (
<VisualizationContainer
className="lnsXyExpression__container"
isReady={state.isReady}
reportTitle={props.args.title}
reportDescription={props.args.description}
>
<MemoizedChart {...props} />
</VisualizationContainer>
);
}
export function XYChart({
data,
args,
formatFactory,
timeZone,
chartsThemeService,
chartsActiveCursorService,
paletteService,
minInterval,
onClickValue,
onSelectRange,
interactive = true,
syncColors,
}: XYChartRenderProps) {
const {
legend,
layers,
fittingFunction,
gridlinesVisibilitySettings,
valueLabels,
hideEndzones,
yLeftExtent,
yRightExtent,
valuesInLegend,
} = args;
const chartRef = useRef<Chart>(null);
const chartTheme = chartsThemeService.useChartsTheme();
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
const darkMode = chartsThemeService.useDarkMode();
const filteredLayers = getFilteredLayers(layers, data);
const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, {
datatables: Object.values(data.tables),
});
if (filteredLayers.length === 0) {
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
return <EmptyPlaceholder icon={icon} />;
}
// use formatting hint of first x axis column to format ticks
const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find(
({ id }) => id === filteredLayers[0].xAccessor
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params);
const layersAlreadyFormatted: Record<string, boolean> = {};
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
const safeXAccessorLabelRenderer = (value: unknown): string =>
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id]
? (value as string)
: xAxisFormatter.convert(value);
const chartHasMoreThanOneSeries =
filteredLayers.length > 1 ||
filteredLayers.some((layer) => layer.accessors.length > 1) ||
filteredLayers.some((layer) => layer.splitAccessor);
const shouldRotate = isHorizontalChart(filteredLayers);
const yAxesConfiguration = getAxesConfiguration(
filteredLayers,
shouldRotate,
data.tables,
formatFactory
);
const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name);
const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || {
x: true,
yLeft: true,
yRight: true,
};
const tickLabelsVisibilitySettings = args.tickLabelsVisibilitySettings || {
x: true,
yLeft: true,
yRight: true,
};
const labelsOrientation = args.labelsOrientation || {
x: 0,
yLeft: 0,
yRight: 0,
};
const filteredBarLayers = filteredLayers.filter((layer) => layer.seriesType.includes('bar'));
const chartHasMoreThanOneBarSeries =
filteredBarLayers.length > 1 ||
filteredBarLayers.some((layer) => layer.accessors.length > 1) ||
filteredBarLayers.some((layer) => layer.splitAccessor);
const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time');
const isHistogramViz = filteredLayers.every((l) => l.isHistogram);
const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain(
filteredLayers,
data,
minInterval,
Boolean(isTimeViz),
Boolean(isHistogramViz)
);
const getYAxesTitles = (
axisSeries: Array<{ layer: string; accessor: string }>,
groupId: string
) => {
const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle;
return (
yTitle ||
axisSeries
.map(
(series) =>
data.tables[series.layer].columns.find((column) => column.id === series.accessor)?.name
)
.filter((name) => Boolean(name))[0]
);
};
const getYAxesStyle = (groupId: string) => {
const style = {
tickLabel: {
visible:
groupId === 'right'
? tickLabelsVisibilitySettings?.yRight
: tickLabelsVisibilitySettings?.yLeft,
rotation:
groupId === 'right'
? args.labelsOrientation?.yRight || 0
: args.labelsOrientation?.yLeft || 0,
},
axisTitle: {
visible:
groupId === 'right'
? axisTitlesVisibilitySettings?.yRight
: axisTitlesVisibilitySettings?.yLeft,
},
};
return style;
};
const getYAxisDomain = (axis: GroupsConfiguration[number]) => {
const extent = axis.groupId === 'left' ? yLeftExtent : yRightExtent;
const hasBarOrArea = Boolean(
axis.series.some((series) => {
const seriesType = filteredLayers.find((l) => l.layerId === series.layer)?.seriesType;
return seriesType?.includes('bar') || seriesType?.includes('area');
})
);
const fit = !hasBarOrArea && extent.mode === 'dataBounds';
let min: undefined | number;
let max: undefined | number;
if (extent.mode === 'custom') {
const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent);
if (!inclusiveZeroError && !boundaryError) {
min = extent.lowerBound;
max = extent.upperBound;
}
}
return {
fit,
min,
max,
};
};
const shouldShowValueLabels =
// No stacked bar charts
filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) &&
// No histogram charts
!isHistogramViz;
const valueLabelsStyling =
shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate);
const colorAssignments = getColorAssignments(args.layers, data, formatFactory);
const clickHandler: ElementClickListener = ([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
const xySeries = series as XYChartSeriesIdentifier;
const xyGeometry = geometry as GeometryValue;
const layer = filteredLayers.find((l) =>
xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
);
if (!layer) {
return;
}
const table = data.tables[layer.layerId];
const xColumn = table.columns.find((col) => col.id === layer.xAccessor);
const currentXFormatter =
layer.xAccessor && layersAlreadyFormatted[layer.xAccessor] && xColumn
? formatFactory(xColumn.meta.params)
: xAxisFormatter;
const rowIndex = table.rows.findIndex((row) => {
if (layer.xAccessor) {
if (layersAlreadyFormatted[layer.xAccessor]) {
// stringify the value to compare with the chart value
return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x;
}
return row[layer.xAccessor] === xyGeometry.x;
}
});
const points = [
{
row: rowIndex,
column: table.columns.findIndex((col) => col.id === layer.xAccessor),
value: layer.xAccessor ? table.rows[rowIndex][layer.xAccessor] : xyGeometry.x,
},
];
if (xySeries.seriesKeys.length > 1) {
const pointValue = xySeries.seriesKeys[0];
points.push({
row: table.rows.findIndex(
(row) => layer.splitAccessor && row[layer.splitAccessor] === pointValue
),
column: table.columns.findIndex((col) => col.id === layer.splitAccessor),
value: pointValue,
});
}
const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta;
const xAxisFieldName = currentColumnMeta?.field;
const isDateField = currentColumnMeta?.type === 'date';
const context: LensFilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
column: point.column,
value: point.value,
table,
})),
timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined,
};
onClickValue(context);
};
const brushHandler: BrushEndListener = ({ x }) => {
if (!x) {
return;
}
const [min, max] = x;
if (!xAxisColumn || !isHistogramViz) {
return;
}
const table = data.tables[filteredLayers[0].layerId];
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor);
const timeFieldName = isTimeViz ? table.columns[xAxisColumnIndex]?.meta?.field : undefined;
const context: LensBrushEvent['data'] = {
range: [min, max],
table,
column: xAxisColumnIndex,
timeFieldName,
};
onSelectRange(context);
};
const legendInsideParams = {
vAlign: legend.verticalAlignment ?? VerticalAlignment.Top,
hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right,
direction: LayoutDirection.Vertical,
floating: true,
floatingColumns: legend?.floatingColumns ?? 1,
} as LegendPositionConfig;
return (
<Chart ref={chartRef}>
<Settings
onPointerUpdate={handleCursorUpdate}
debugState={window._echDebugStateFlag ?? false}
showLegend={
legend.isVisible && !legend.showSingleSeries
? chartHasMoreThanOneSeries
: legend.isVisible
}
legendPosition={legend?.isInside ? legendInsideParams : legend.position}
theme={{
...chartTheme,
barSeriesStyle: {
...chartTheme.barSeriesStyle,
...valueLabelsStyling,
},
background: {
color: undefined, // removes background for embeddables
},
legend: {
labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
},
}}
baseTheme={chartBaseTheme}
tooltip={{
boundary: document.getElementById('app-fixed-viewport') ?? undefined,
headerFormatter: (d) => safeXAccessorLabelRenderer(d.value),
}}
allowBrushingLastHistogramBucket={Boolean(isTimeViz)}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
onBrushEnd={interactive ? brushHandler : undefined}
onElementClick={interactive ? clickHandler : undefined}
legendAction={getLegendAction(
filteredLayers,
data.tables,
onClickValue,
formatFactory,
layersAlreadyFormatted
)}
showLegendExtra={isHistogramViz && valuesInLegend}
/>
<Axis
id="x"
position={shouldRotate ? Position.Left : Position.Bottom}
title={xTitle}
gridLine={{
visible: gridlinesVisibilitySettings?.x,
strokeWidth: 2,
}}
hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor}
tickFormat={(d) => safeXAccessorLabelRenderer(d)}
style={{
tickLabel: {
visible: tickLabelsVisibilitySettings?.x,
rotation: labelsOrientation?.x,
},
axisTitle: {
visible: axisTitlesVisibilitySettings.x,
},
}}
/>
{yAxesConfiguration.map((axis) => {
return (
<Axis
key={axis.groupId}
id={axis.groupId}
groupId={axis.groupId}
position={axis.position}
title={getYAxesTitles(axis.series, axis.groupId)}
gridLine={{
visible:
axis.groupId === 'right'
? gridlinesVisibilitySettings?.yRight
: gridlinesVisibilitySettings?.yLeft,
}}
hide={filteredLayers[0].hide}
tickFormat={(d) => axis.formatter?.convert(d) || ''}
style={getYAxesStyle(axis.groupId)}
domain={getYAxisDomain(axis)}
/>
);
})}
{!hideEndzones && (
<XyEndzones
baseDomain={rawXDomain}
extendedDomain={xDomain}
darkMode={darkMode}
histogramMode={filteredLayers.every(
(layer) =>
layer.isHistogram &&
(layer.seriesType.includes('stacked') || !layer.splitAccessor) &&
(layer.seriesType.includes('stacked') ||
!layer.seriesType.includes('bar') ||
!chartHasMoreThanOneBarSeries)
)}
/>
)}
{filteredLayers.flatMap((layer, layerIndex) =>
layer.accessors.map((accessor, accessorIndex) => {
const {
splitAccessor,
seriesType,
accessors,
xAccessor,
layerId,
columnToLabel,
yScaleType,
xScaleType,
isHistogram,
palette,
} = layer;
const columnToLabelMap: Record<string, string> = columnToLabel
? JSON.parse(columnToLabel)
: {};
const table = data.tables[layerId];
const isPrimitive = (value: unknown): boolean =>
value != null && typeof value !== 'object';
// what if row values are not primitive? That is the case of, for instance, Ranges
// remaps them to their serialized version with the formatHint metadata
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
const tableConverted: Datatable = {
...table,
rows: table.rows.map((row: DatatableRow) => {
const newRow = { ...row };
for (const column of table.columns) {
const record = newRow[column.id];
if (
record != null &&
// pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level
(!isPrimitive(record) || (column.id === xAccessor && xScaleType === 'ordinal'))
) {
newRow[column.id] = formatFactory(column.meta.params).convert(record);
}
}
return newRow;
}),
};
// save the id of the layer with the custom table
table.columns.reduce<Record<string, boolean>>(
(alreadyFormatted: Record<string, boolean>, { id }) => {
if (alreadyFormatted[id]) {
return alreadyFormatted;
}
alreadyFormatted[id] = table.rows.some(
(row, i) => row[id] !== tableConverted.rows[i][id]
);
return alreadyFormatted;
},
layersAlreadyFormatted
);
const isStacked = seriesType.includes('stacked');
const isPercentage = seriesType.includes('percentage');
const isBarChart = seriesType.includes('bar');
const enableHistogramMode =
isHistogram &&
(isStacked || !splitAccessor) &&
(isStacked || !isBarChart || !chartHasMoreThanOneBarSeries);
// For date histogram chart type, we're getting the rows that represent intervals without data.
// To not display them in the legend, they need to be filtered out.
const rows = tableConverted.rows.filter(
(row) =>
!(xAccessor && typeof row[xAccessor] === 'undefined') &&
!(
splitAccessor &&
typeof row[splitAccessor] === 'undefined' &&
typeof row[accessor] === 'undefined'
)
);
if (!xAccessor) {
rows.forEach((row) => {
row.unifiedX = i18n.translate('xpack.lens.xyChart.emptyXLabel', {
defaultMessage: '(empty)',
});
});
}
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
);
const seriesProps: SeriesSpec = {
splitSeriesAccessors: splitAccessor ? [splitAccessor] : [],
stackAccessors: isStacked ? [xAccessor as string] : [],
id: `${splitAccessor}-${accessor}`,
xAccessor: xAccessor || 'unifiedX',
yAccessors: [accessor],
data: rows,
xScaleType: xAccessor ? xScaleType : 'ordinal',
yScaleType,
color: ({ yAccessor, seriesKeys }) => {
const overwriteColor = getSeriesColor(layer, accessor);
if (overwriteColor !== null) {
return overwriteColor;
}
const colorAssignment = colorAssignments[palette.name];
const seriesLayers: SeriesLayer[] = [
{
name: splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
rankAtDepth: colorAssignment.getRank(
layer,
String(seriesKeys[0]),
String(yAccessor)
),
},
];
return paletteService.get(palette.name).getCategoricalColor(
seriesLayers,
{
maxDepth: 1,
behindText: false,
totalSeries: colorAssignment.totalSeriesCount,
syncColors,
},
palette.params
);
},
groupId: yAxis?.groupId,
enableHistogramMode,
stackMode: isPercentage ? StackMode.Percentage : undefined,
timeZone,
areaSeriesStyle: {
point: {
visible: !xAccessor,
radius: 5,
},
...(args.fillOpacity && { area: { opacity: args.fillOpacity } }),
},
lineSeriesStyle: {
point: {
visible: !xAccessor,
radius: 5,
},
},
name(d) {
const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params;
// For multiple y series, the name of the operation is used on each, either:
// * Key - Y name
// * Formatted value - Y name
if (accessors.length > 1) {
const result = d.seriesKeys
.map((key: string | number, i) => {
if (
i === 0 &&
splitHint &&
splitAccessor &&
!layersAlreadyFormatted[splitAccessor]
) {
return formatFactory(splitHint).convert(key);
}
return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? '';
})
.join(' - ');
return result;
}
// For formatted split series, format the key
// This handles splitting by dates, for example
if (splitHint) {
if (splitAccessor && layersAlreadyFormatted[splitAccessor]) {
return d.seriesKeys[0];
}
return formatFactory(splitHint).convert(d.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 splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? '';
},
};
const index = `${layerIndex}-${accessorIndex}`;
const curveType = args.curveType ? CurveType[args.curveType] : undefined;
switch (seriesType) {
case 'line':
return (
<LineSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction)}
curve={curveType}
/>
);
case 'bar':
case 'bar_stacked':
case 'bar_percentage_stacked':
case 'bar_horizontal':
case 'bar_horizontal_stacked':
case 'bar_horizontal_percentage_stacked':
const valueLabelsSettings = {
displayValueSettings: {
// This format double fixes two issues in elastic-chart
// * when rotating the chart, the formatter is not correctly picked
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
showValueLabel: shouldShowValueLabels && valueLabels !== 'hide',
isValueContainedInElement: false,
isAlternatingValueLabel: false,
overflowConstraints: [
LabelOverflowConstraint.ChartEdges,
LabelOverflowConstraint.BarGeometry,
],
},
};
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
case 'area_stacked':
case 'area_percentage_stacked':
return (
<AreaSeries
key={index}
{...seriesProps}
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction)}
curve={curveType}
/>
);
case 'area':
return (
<AreaSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction)}
curve={curveType}
/>
);
default:
return assertNever(seriesType);
}
})
)}
</Chart>
);
}
function getFilteredLayers(layers: LayerArgs[], data: LensMultiTable) {
return layers.filter(({ layerId, xAccessor, accessors, splitAccessor, layerType }) => {
return (
layerType === layerTypes.DATA &&
!(
!accessors.length ||
!data.tables[layerId] ||
data.tables[layerId].rows.length === 0 ||
(xAccessor &&
data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) ||
// stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty
(!xAccessor &&
splitAccessor &&
data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined'))
)
);
});
}
function assertNever(x: never): never {
throw new Error('Unexpected series type: ' + x);
}