mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[XY] Add addTimeMarker
arg (#131495)
* Add `addTimeMarker` arg * Some fixes * Update validation * Fix snapshots * Some fixes after merge * Add unit tests * Fix CI * Update src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx Co-authored-by: Yaroslav Kuznietsov <kuznetsov.yaroslav.yk@gmail.com> * Fixed tests * Fix checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yaroslav Kuznietsov <kuznetsov.yaroslav.yk@gmail.com>
This commit is contained in:
parent
de90ea592b
commit
f75b6fa156
18 changed files with 580 additions and 456 deletions
|
@ -35,7 +35,7 @@ export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable =
|
|||
id: 'c',
|
||||
name: 'c',
|
||||
meta: {
|
||||
type: 'string',
|
||||
type: 'date',
|
||||
field: 'order_date',
|
||||
sourceParams: { type: 'date-histogram', params: { interval: 'auto' } },
|
||||
params: { id: 'string' },
|
||||
|
@ -128,8 +128,8 @@ export const createArgsWithLayers = (
|
|||
|
||||
export function sampleArgs() {
|
||||
const data = createSampleDatatableWithRows([
|
||||
{ a: 1, b: 2, c: 'I', d: 'Foo' },
|
||||
{ a: 1, b: 5, c: 'J', d: 'Bar' },
|
||||
{ a: 1, b: 2, c: 1652034840000, d: 'Foo' },
|
||||
{ a: 1, b: 5, c: 1652122440000, d: 'Bar' },
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`xyVis it should throw error if addTimeMarker applied for not time chart 1`] = `"Only time charts can have current time marker"`;
|
||||
|
||||
exports[`xyVis it should throw error if markSizeRatio is lower then 1 or greater then 100 1`] = `"Mark size ratio must be greater or equal to 1 and less or equal to 100"`;
|
||||
|
||||
exports[`xyVis it should throw error if markSizeRatio is lower then 1 or greater then 100 2`] = `"Mark size ratio must be greater or equal to 1 and less or equal to 100"`;
|
||||
|
|
|
@ -36,7 +36,6 @@ export const commonDataLayerArgs: Omit<
|
|||
xScaleType: {
|
||||
options: [...Object.values(XScaleTypes)],
|
||||
help: strings.getXScaleTypeHelp(),
|
||||
default: XScaleTypes.ORDINAL,
|
||||
strict: true,
|
||||
},
|
||||
isHistogram: {
|
||||
|
|
|
@ -128,6 +128,11 @@ export const commonXYArgs: CommonXYFn['args'] = {
|
|||
types: ['string'],
|
||||
help: strings.getAriaLabelHelp(),
|
||||
},
|
||||
addTimeMarker: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: strings.getAddTimeMakerHelp(),
|
||||
},
|
||||
markSizeRatio: {
|
||||
types: ['number'],
|
||||
help: strings.getMarkSizeRatioHelp(),
|
||||
|
|
|
@ -7,15 +7,16 @@
|
|||
*/
|
||||
|
||||
import { XY_VIS_RENDERER } from '../constants';
|
||||
import { appendLayerIds, getDataLayers } from '../helpers';
|
||||
import { LayeredXyVisFn } from '../types';
|
||||
import { logDatatables } from '../utils';
|
||||
import {
|
||||
validateMarkSizeRatioLimits,
|
||||
validateAddTimeMarker,
|
||||
validateMinTimeBarInterval,
|
||||
hasBarLayer,
|
||||
errors,
|
||||
} from './validate';
|
||||
import { appendLayerIds, getDataLayers } from '../helpers';
|
||||
|
||||
export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => {
|
||||
const layers = appendLayerIds(args.layers ?? [], 'layers');
|
||||
|
@ -24,6 +25,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers)
|
|||
|
||||
const dataLayers = getDataLayers(layers);
|
||||
const hasBar = hasBarLayer(dataLayers);
|
||||
validateAddTimeMarker(dataLayers, args.addTimeMarker);
|
||||
validateMarkSizeRatioLimits(args.markSizeRatio);
|
||||
validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval);
|
||||
const hasMarkSizeAccessors =
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
CommonXYDataLayerConfigResult,
|
||||
ValueLabelMode,
|
||||
CommonXYDataLayerConfig,
|
||||
ExtendedDataLayerConfigResult,
|
||||
} from '../types';
|
||||
import { isTimeChart } from '../helpers';
|
||||
|
||||
|
@ -58,6 +59,10 @@ export const errors = {
|
|||
i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', {
|
||||
defaultMessage: 'Only line charts can be fit to the data bounds',
|
||||
}),
|
||||
timeMarkerForNotTimeChartsError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.xyVis.errors.timeMarkerForNotTimeChartsError', {
|
||||
defaultMessage: 'Only time charts can have current time marker',
|
||||
}),
|
||||
isInvalidIntervalError: () =>
|
||||
i18n.translate('expressionXY.reusable.function.xyVis.errors.isInvalidIntervalError', {
|
||||
defaultMessage:
|
||||
|
@ -135,6 +140,15 @@ export const validateValueLabels = (
|
|||
}
|
||||
};
|
||||
|
||||
export const validateAddTimeMarker = (
|
||||
dataLayers: Array<DataLayerConfigResult | ExtendedDataLayerConfigResult>,
|
||||
addTimeMarker?: boolean
|
||||
) => {
|
||||
if (addTimeMarker && !isTimeChart(dataLayers)) {
|
||||
throw new Error(errors.timeMarkerForNotTimeChartsError());
|
||||
}
|
||||
};
|
||||
|
||||
export const validateMarkSizeForChartType = (
|
||||
markSizeAccessor: ExpressionValueVisDimension | string | undefined,
|
||||
seriesType: SeriesType
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import { xyVisFunction } from '.';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
|
||||
import { sampleArgs, sampleLayer } from '../__mocks__';
|
||||
import { XY_VIS } from '../constants';
|
||||
|
@ -15,26 +14,10 @@ import { XY_VIS } from '../constants';
|
|||
describe('xyVis', () => {
|
||||
test('it renders with the specified data and args', async () => {
|
||||
const { data, args } = sampleArgs();
|
||||
const newData = {
|
||||
...data,
|
||||
type: 'datatable',
|
||||
|
||||
columns: data.columns.map((c) =>
|
||||
c.id !== 'c'
|
||||
? c
|
||||
: {
|
||||
...c,
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
),
|
||||
} as Datatable;
|
||||
|
||||
const { layers, ...rest } = args;
|
||||
const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer;
|
||||
const result = await xyVisFunction.fn(
|
||||
newData,
|
||||
data,
|
||||
{ ...rest, ...restLayerArgs, referenceLines: [], annotationLayers: [] },
|
||||
createMockExecutionContext()
|
||||
);
|
||||
|
@ -45,7 +28,7 @@ describe('xyVis', () => {
|
|||
value: {
|
||||
args: {
|
||||
...rest,
|
||||
layers: [{ layerType, table: newData, layerId: 'dataLayers-0', type, ...restLayerArgs }],
|
||||
layers: [{ layerType, table: data, layerId: 'dataLayers-0', type, ...restLayerArgs }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -120,6 +103,25 @@ describe('xyVis', () => {
|
|||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('it should throw error if addTimeMarker applied for not time chart', async () => {
|
||||
const { data, args } = sampleArgs();
|
||||
const { layers, ...rest } = args;
|
||||
const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer;
|
||||
expect(
|
||||
xyVisFunction.fn(
|
||||
data,
|
||||
{
|
||||
...rest,
|
||||
...restLayerArgs,
|
||||
addTimeMarker: true,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('it should throw error if splitRowAccessor is pointing to the absent column', async () => {
|
||||
const { data, args } = sampleArgs();
|
||||
const { layers, ...rest } = args;
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
validateFillOpacity,
|
||||
validateMarkSizeRatioLimits,
|
||||
validateValueLabels,
|
||||
validateAddTimeMarker,
|
||||
validateMinTimeBarInterval,
|
||||
validateMarkSizeForChartType,
|
||||
validateMarkSizeRatioWithAccessor,
|
||||
|
@ -107,6 +108,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
|
|||
validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers);
|
||||
validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers);
|
||||
validateFillOpacity(args.fillOpacity, hasArea);
|
||||
validateAddTimeMarker(dataLayers, args.addTimeMarker);
|
||||
validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval);
|
||||
|
||||
const hasNotHistogramBars = !hasHistogramBarLayer(dataLayers);
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { XScaleTypes } from '../constants';
|
||||
import { CommonXYDataLayerConfigResult } from '../types';
|
||||
|
||||
export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) {
|
||||
return layers.every<CommonXYDataLayerConfigResult>(
|
||||
(l): l is CommonXYDataLayerConfigResult =>
|
||||
l.table.columns.find((col) => col.id === l.xAccessor)?.meta.type === 'date' &&
|
||||
l.xScaleType === XScaleTypes.TIME
|
||||
(l.xAccessor
|
||||
? getColumnByAccessor(l.xAccessor, l.table.columns)?.meta.type === 'date'
|
||||
: false) &&
|
||||
(!l.xScaleType || l.xScaleType === XScaleTypes.TIME)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,10 @@ export const strings = {
|
|||
i18n.translate('expressionXY.xyVis.ariaLabel.help', {
|
||||
defaultMessage: 'Specifies the aria label of the xy chart',
|
||||
}),
|
||||
getAddTimeMakerHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.addTimeMaker.help', {
|
||||
defaultMessage: 'Show time marker',
|
||||
}),
|
||||
getMarkSizeRatioHelp: () =>
|
||||
i18n.translate('expressionXY.xyVis.markSizeRatio.help', {
|
||||
defaultMessage: 'Specifies the ratio of the dots at the line and area charts',
|
||||
|
|
|
@ -207,6 +207,7 @@ export interface XYArgs extends DataLayerArgs {
|
|||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
addTimeMarker?: boolean;
|
||||
markSizeRatio?: number;
|
||||
minTimeBarInterval?: string;
|
||||
splitRowAccessor?: ExpressionValueVisDimension | string;
|
||||
|
@ -236,6 +237,7 @@ export interface LayeredXYArgs {
|
|||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
addTimeMarker?: boolean;
|
||||
markSizeRatio?: number;
|
||||
minTimeBarInterval?: string;
|
||||
}
|
||||
|
@ -263,6 +265,7 @@ export interface XYProps {
|
|||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
ariaLabel?: string;
|
||||
addTimeMarker?: boolean;
|
||||
markSizeRatio?: number;
|
||||
minTimeBarInterval?: string;
|
||||
splitRowAccessor?: ExpressionValueVisDimension | string;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,7 @@ import {
|
|||
FittingFunction,
|
||||
ValueLabelMode,
|
||||
XYCurveType,
|
||||
XScaleType,
|
||||
} from '../../common';
|
||||
import { SeriesTypes, ValueLabelModes } from '../../common/constants';
|
||||
import {
|
||||
|
@ -49,6 +50,7 @@ interface Props {
|
|||
fillOpacity?: number;
|
||||
shouldShowValueLabels?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
defaultXScaleType: XScaleType;
|
||||
}
|
||||
|
||||
export const DataLayers: FC<Props> = ({
|
||||
|
@ -67,6 +69,7 @@ export const DataLayers: FC<Props> = ({
|
|||
shouldShowValueLabels,
|
||||
formattedDatatables,
|
||||
chartHasMoreThanOneBarSeries,
|
||||
defaultXScaleType,
|
||||
}) => {
|
||||
const colorAssignments = getColorAssignments(layers, formatFactory);
|
||||
return (
|
||||
|
@ -104,6 +107,7 @@ export const DataLayers: FC<Props> = ({
|
|||
timeZone,
|
||||
emphasizeFitting,
|
||||
fillOpacity,
|
||||
defaultXScaleType,
|
||||
});
|
||||
|
||||
const index = `${layer.layerId}-${accessorIndex}`;
|
||||
|
|
|
@ -1967,17 +1967,10 @@ describe('XYChart component', () => {
|
|||
test('it should pass the formatter function to the axis', () => {
|
||||
const { args } = sampleArgs();
|
||||
|
||||
const instance = shallow(<XYChart {...defaultProps} args={{ ...args }} />);
|
||||
shallow(<XYChart {...defaultProps} args={{ ...args }} />);
|
||||
|
||||
const tickFormatter = instance.find(Axis).first().prop('tickFormat');
|
||||
|
||||
if (!tickFormatter) {
|
||||
throw new Error('tickFormatter prop not found');
|
||||
}
|
||||
|
||||
tickFormatter('I');
|
||||
|
||||
expect(convertSpy).toHaveBeenCalledWith('I');
|
||||
expect(convertSpy).toHaveBeenCalledWith(1652034840000);
|
||||
expect(convertSpy).toHaveBeenCalledWith(1652122440000);
|
||||
});
|
||||
|
||||
test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => {
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
LegendSizeToPixels,
|
||||
} from '@kbn/visualizations-plugin/common/constants';
|
||||
import type { FilterEvent, BrushEvent, FormatFactory } from '../types';
|
||||
import { isTimeChart } from '../../common/helpers';
|
||||
import type {
|
||||
CommonXYDataLayerConfig,
|
||||
ExtendedYConfig,
|
||||
|
@ -81,8 +82,10 @@ import {
|
|||
OUTSIDE_RECT_ANNOTATION_WIDTH,
|
||||
OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION,
|
||||
} from './annotations';
|
||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants';
|
||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants';
|
||||
import { DataLayers } from './data_layers';
|
||||
import { XYCurrentTime } from './xy_current_time';
|
||||
|
||||
import './xy_chart.scss';
|
||||
|
||||
declare global {
|
||||
|
@ -249,7 +252,10 @@ export function XYChart({
|
|||
filteredBarLayers.some((layer) => layer.accessors.length > 1) ||
|
||||
filteredBarLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor);
|
||||
|
||||
const isTimeViz = Boolean(dataLayers.every((l) => l.xScaleType === 'time'));
|
||||
const isTimeViz = isTimeChart(dataLayers);
|
||||
|
||||
const defaultXScaleType = isTimeViz ? XScaleTypes.TIME : XScaleTypes.ORDINAL;
|
||||
|
||||
const isHistogramViz = dataLayers.every((l) => l.isHistogram);
|
||||
|
||||
const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain(
|
||||
|
@ -604,6 +610,11 @@ export function XYChart({
|
|||
ariaLabel={args.ariaLabel}
|
||||
ariaUseDefaultSummary={!args.ariaLabel}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={Boolean(args.addTimeMarker && isTimeViz)}
|
||||
isDarkMode={darkMode}
|
||||
domain={rawXDomain}
|
||||
/>
|
||||
|
||||
<Axis
|
||||
id="x"
|
||||
|
@ -679,6 +690,7 @@ export function XYChart({
|
|||
shouldShowValueLabels={shouldShowValueLabels}
|
||||
formattedDatatables={formattedDatatables}
|
||||
chartHasMoreThanOneBarSeries={chartHasMoreThanOneBarSeries}
|
||||
defaultXScaleType={defaultXScaleType}
|
||||
/>
|
||||
)}
|
||||
{referenceLineLayers.length ? (
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { DomainRange } from '@elastic/charts';
|
||||
import { CurrentTime } from '@kbn/charts-plugin/public';
|
||||
|
||||
interface XYCurrentTime {
|
||||
enabled: boolean;
|
||||
isDarkMode: boolean;
|
||||
domain?: DomainRange;
|
||||
}
|
||||
|
||||
export const XYCurrentTime: FC<XYCurrentTime> = ({ enabled, isDarkMode, domain }) => {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domainEnd = domain && 'max' in domain ? domain.max : undefined;
|
||||
return <CurrentTime isDarkMode={isDarkMode} domainEnd={domainEnd} />;
|
||||
};
|
|
@ -53,6 +53,7 @@ type GetSeriesPropsFn = (config: {
|
|||
emphasizeFitting?: boolean;
|
||||
fillOpacity?: number;
|
||||
formattedDatatableInfo: DatatableWithFormatInfo;
|
||||
defaultXScaleType: XScaleType;
|
||||
}) => SeriesSpec;
|
||||
|
||||
type GetSeriesNameFn = (
|
||||
|
@ -280,6 +281,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
emphasizeFitting,
|
||||
fillOpacity,
|
||||
formattedDatatableInfo,
|
||||
defaultXScaleType,
|
||||
}): SeriesSpec => {
|
||||
const { table, markSizeAccessor } = layer;
|
||||
const isStacked = layer.seriesType.includes('stacked');
|
||||
|
@ -342,7 +344,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
markSizeAccessor: markSizeColumnId,
|
||||
markFormat: (value) => markFormatter.convert(value),
|
||||
data: rows,
|
||||
xScaleType: xColumnId ? layer.xScaleType : 'ordinal',
|
||||
xScaleType: xColumnId ? layer.xScaleType ?? defaultXScaleType : 'ordinal',
|
||||
yScaleType:
|
||||
formatter?.id === 'bytes' && yAxis?.scale === ScaleType.Linear
|
||||
? ScaleType.LinearBinary
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
import { search } from '@kbn/data-plugin/public';
|
||||
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { XYChartProps } from '../../common';
|
||||
import { isTimeChart } from '../../common/helpers';
|
||||
import { getFilteredLayers } from './layers';
|
||||
import { isDataLayer } from './visualization';
|
||||
import { isDataLayer, getDataLayers } from './visualization';
|
||||
|
||||
export function calculateMinInterval({ args: { layers, minTimeBarInterval } }: XYChartProps) {
|
||||
const filteredLayers = getFilteredLayers(layers);
|
||||
if (filteredLayers.length === 0) return;
|
||||
const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time');
|
||||
const isTimeViz = isTimeChart(getDataLayers(filteredLayers));
|
||||
const xColumn =
|
||||
isDataLayer(filteredLayers[0]) &&
|
||||
filteredLayers[0].xAccessor &&
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue