diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts new file mode 100644 index 0000000000000..393a6ee06cf58 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts @@ -0,0 +1,386 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { VisConfig } from '../types'; + +export const getVisConfig = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3', + column: 1, + title: 'Average products.base_price', + format: { + id: 'number', + }, + aggType: 'avg', + aggId: '3', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getVisConfigPercentiles = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3.1', + column: 1, + title: '1st percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.1', + params: {}, + }, + { + accessor: 'col-2-3.5', + column: 2, + title: '5th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.5', + params: {}, + }, + { + accessor: 'col-3-3.25', + column: 3, + title: '25th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.25', + params: {}, + }, + { + accessor: 'col-4-3.50', + column: 4, + title: '50th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.50', + params: {}, + }, + { + accessor: 'col-5-3.75', + column: 5, + title: '75th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.75', + params: {}, + }, + { + accessor: 'col-6-3.95', + column: 6, + title: '95th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.95', + params: {}, + }, + { + accessor: 'col-7-3.99', + column: 7, + title: '99th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.99', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getPercentilesData = () => { + return [ + { + 'col-0-2': 1610961900000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 38.49609375, + 'col-5-3.75': 65, + 'col-6-3.95': 65, + 'col-7-3.99': 65, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3.1': 28.984375000000004, + 'col-2-3.5': 28.984375, + 'col-3-3.25': 28.984375, + 'col-4-3.50': 30.9921875, + 'col-5-3.75': 41.5, + 'col-6-3.95': 50, + 'col-7-3.99': 50, + }, + { + 'col-0-2': 1610963280000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 12.9921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + { + 'col-0-2': 1610964180000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 14.9921875, + 'col-4-3.50': 15.98828125, + 'col-5-3.75': 24.984375, + 'col-6-3.95': 85, + 'col-7-3.99': 85, + }, + { + 'col-0-2': 1610964420000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 23.99609375, + 'col-5-3.75': 42, + 'col-6-3.95': 42, + 'col-7-3.99': 42, + }, + { + 'col-0-2': 1610964600000, + 'col-1-3.1': 10.9921875, + 'col-2-3.5': 10.992187500000002, + 'col-3-3.25': 10.9921875, + 'col-4-3.50': 12.4921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + ]; +}; diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx new file mode 100644 index 0000000000000..d76ea49a2f110 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { AreaSeries, BarSeries, CurveType } from '@elastic/charts'; +import { DatatableRow } from '../../../expressions/public'; +import { renderAllSeries } from './render_all_series'; +import { + getVisConfig, + getVisConfigPercentiles, + getPercentilesData, +} from './render_all_series.test.mocks'; +import { SeriesParam, VisConfig } from '../types'; + +const defaultSeriesParams = [ + { + data: { + id: '3', + label: 'Label', + }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + lineWidth: 2, + mode: 'stacked', + show: true, + showCircles: true, + type: 'area', + valueAxis: 'ValueAxis-1', + }, +] as SeriesParam[]; + +const defaultData = [ + { + 'col-0-2': 1610960220000, + 'col-1-3': 26.984375, + }, + { + 'col-0-2': 1610961300000, + 'col-1-3': 30.99609375, + }, + { + 'col-0-2': 1610961900000, + 'col-1-3': 38.49609375, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3': 35.2421875, + }, +]; + +describe('renderAllSeries', function () { + const getAllSeries = (visConfig: VisConfig, params: SeriesParam[], data: DatatableRow[]) => { + return renderAllSeries( + visConfig, + params, + data, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + }; + + it('renders an area Series and not a bar series if type is area', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(1); + expect(wrapper.find(BarSeries).length).toBe(0); + }); + + it('renders a bar Series in case of histogram', () => { + const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram' }]; + + const renderBarSeries = renderAllSeries( + getVisConfig(), + barSeriesParams as SeriesParam[], + defaultData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderBarSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(0); + expect(wrapper.find(BarSeries).length).toBe(1); + }); + + it('renders the correct yAccessors for not percentile aggs', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual(['col-1-3']); + }); + + it('renders the correct yAccessors for percentile aggs', () => { + const percentilesConfig = getVisConfigPercentiles(); + const percentilesData = getPercentilesData(); + const renderPercentileSeries = renderAllSeries( + percentilesConfig, + defaultSeriesParams as SeriesParam[], + percentilesData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderPercentileSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual([ + 'col-1-3.1', + 'col-2-3.5', + 'col-3-3.25', + 'col-4-3.50', + 'col-5-3.75', + 'col-6-3.95', + 'col-7-3.99', + ]); + }); + + it('defaults the CurveType to linear', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('curve')).toEqual(CurveType.LINEAR); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx index 264fa539c1980..fb884bb235971 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx @@ -71,13 +71,15 @@ export const renderAllSeries = ( interpolate, type, }) => { - const yAspect = aspects.y.find(({ aggId }) => aggId === paramId); - - if (!show || !yAspect || yAspect.accessor === null) { + const yAspects = aspects.y.filter( + ({ aggId, accessor }) => aggId?.includes(paramId) && accessor !== null + ); + if (!show || !yAspects.length) { return null; } + const yAccessors = yAspects.map((aspect) => aspect.accessor) as string[]; - const id = `${type}-${yAspect.accessor}`; + const id = `${type}-${yAccessors[0]}`; const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; const isStacked = mode === 'stacked' || yAxisScale?.mode === 'percentage'; const stackMode = yAxisScale?.mode === 'normal' ? undefined : yAxisScale?.mode; @@ -94,13 +96,13 @@ export const renderAllSeries = ( id={id} name={getSeriesName} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} groupId={pseudoGroupId} useDefaultGroupDomain={useDefaultGroupDomain} xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} splitSeriesAccessors={splitSeriesAccessors} data={data} timeZone={timeZone} @@ -125,7 +127,7 @@ export const renderAllSeries = ( id={id} fit={fittingFunction} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} name={getSeriesName} curve={getCurveType(interpolate)} groupId={pseudoGroupId} @@ -133,7 +135,7 @@ export const renderAllSeries = ( xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} markSizeAccessor={markSizeAccessor} markFormat={aspects.z?.formatter} splitSeriesAccessors={splitSeriesAccessors} diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 0cdabd2fa409e..6f994707cbb72 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -296,10 +296,38 @@ const VisComponent = (props: VisComponentProps) => { ] ); const xAccessor = getXAccessor(config.aspects.x); - const splitSeriesAccessors = config.aspects.series - ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) - : []; + const splitSeriesAccessors = useMemo( + () => + config.aspects.series + ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) + : [], + [config.aspects.series] + ); + + const renderSeries = useMemo( + () => + renderAllSeries( + config, + visParams.seriesParams, + visData.rows, + getSeriesName, + getSeriesColor, + timeZone, + xAccessor, + splitSeriesAccessors + ), + [ + config, + getSeriesColor, + getSeriesName, + splitSeriesAccessors, + timeZone, + visData.rows, + visParams.seriesParams, + xAccessor, + ] + ); return (
{ {config.yAxes.map((axisProps) => ( ))} - {renderAllSeries( - config, - visParams.seriesParams, - visData.rows, - getSeriesName, - getSeriesColor, - timeZone, - xAccessor, - splitSeriesAccessors - )} + {renderSeries}
);