* Updating everything except the license headers themselves * Applying ESLint rules * Manually replacing the stragglers
1912 lines
57 KiB
TypeScript
1912 lines
57 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 {
|
|
AreaSeries,
|
|
Axis,
|
|
BarSeries,
|
|
Position,
|
|
LineSeries,
|
|
Settings,
|
|
ScaleType,
|
|
GeometryValue,
|
|
XYChartSeriesIdentifier,
|
|
SeriesNameFn,
|
|
Fit,
|
|
} from '@elastic/charts';
|
|
import { PaletteOutput } from 'src/plugins/charts/public';
|
|
import {
|
|
calculateMinInterval,
|
|
xyChart,
|
|
XYChart,
|
|
XYChartProps,
|
|
XYChartRenderProps,
|
|
} from './expression';
|
|
import { LensMultiTable } from '../types';
|
|
import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public';
|
|
import React from 'react';
|
|
import { shallow } from 'enzyme';
|
|
import {
|
|
XYArgs,
|
|
LegendConfig,
|
|
legendConfig,
|
|
layerConfig,
|
|
LayerArgs,
|
|
AxesSettingsConfig,
|
|
tickLabelsConfig,
|
|
gridlinesConfig,
|
|
} from './types';
|
|
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
|
|
import { mountWithIntl } from '@kbn/test/jest';
|
|
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
|
import { EmptyPlaceholder } from '../shared_components/empty_placeholder';
|
|
|
|
const onClickValue = jest.fn();
|
|
const onSelectRange = jest.fn();
|
|
|
|
const chartsThemeService = chartPluginMock.createSetupContract().theme;
|
|
const paletteService = chartPluginMock.createPaletteRegistry();
|
|
|
|
const mockPaletteOutput: PaletteOutput = {
|
|
type: 'palette',
|
|
name: 'mock',
|
|
params: {},
|
|
};
|
|
|
|
const dateHistogramData: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
timeLayer: {
|
|
type: 'datatable',
|
|
rows: [
|
|
{
|
|
xAccessorId: 1585758120000,
|
|
splitAccessorId: "Men's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585758360000,
|
|
splitAccessorId: "Women's Accessories",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585758360000,
|
|
splitAccessorId: "Women's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585759380000,
|
|
splitAccessorId: "Men's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585759380000,
|
|
splitAccessorId: "Men's Shoes",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585759380000,
|
|
splitAccessorId: "Women's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585760700000,
|
|
splitAccessorId: "Men's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585760760000,
|
|
splitAccessorId: "Men's Clothing",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585760760000,
|
|
splitAccessorId: "Men's Shoes",
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 1585761120000,
|
|
splitAccessorId: "Men's Shoes",
|
|
yAccessorId: 1,
|
|
},
|
|
],
|
|
columns: [
|
|
{
|
|
id: 'xAccessorId',
|
|
name: 'order_date per minute',
|
|
meta: {
|
|
type: 'date',
|
|
field: 'order_date',
|
|
source: 'esaggs',
|
|
index: 'indexPatternId',
|
|
sourceParams: {
|
|
indexPatternId: 'indexPatternId',
|
|
type: 'date_histogram',
|
|
params: {
|
|
field: 'order_date',
|
|
timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' },
|
|
useNormalizedEsInterval: true,
|
|
scaleMetricValues: false,
|
|
interval: '1m',
|
|
drop_partials: false,
|
|
min_doc_count: 0,
|
|
extended_bounds: {},
|
|
},
|
|
},
|
|
params: { id: 'date', params: { pattern: 'HH:mm' } },
|
|
},
|
|
},
|
|
{
|
|
id: 'splitAccessorId',
|
|
name: 'Top values of category.keyword',
|
|
meta: {
|
|
type: 'string',
|
|
field: 'category.keyword',
|
|
source: 'esaggs',
|
|
index: 'indexPatternId',
|
|
sourceParams: {
|
|
indexPatternId: 'indexPatternId',
|
|
type: 'terms',
|
|
params: {
|
|
field: 'category.keyword',
|
|
orderBy: 'yAccessorId',
|
|
order: 'desc',
|
|
size: 3,
|
|
otherBucket: false,
|
|
otherBucketLabel: 'Other',
|
|
missingBucket: false,
|
|
missingBucketLabel: 'Missing',
|
|
},
|
|
},
|
|
params: {
|
|
id: 'terms',
|
|
params: {
|
|
id: 'string',
|
|
otherBucketLabel: 'Other',
|
|
missingBucketLabel: 'Missing',
|
|
parsedUrl: {
|
|
origin: 'http://localhost:5601',
|
|
pathname: '/jiy/app/kibana',
|
|
basePath: '/jiy',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'yAccessorId',
|
|
name: 'Count of records',
|
|
meta: {
|
|
type: 'number',
|
|
source: 'esaggs',
|
|
index: 'indexPatternId',
|
|
sourceParams: {
|
|
indexPatternId: 'indexPatternId',
|
|
params: {},
|
|
},
|
|
params: { id: 'number' },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
dateRange: {
|
|
fromDate: new Date('2020-04-01T16:14:16.246Z'),
|
|
toDate: new Date('2020-04-01T17:15:41.263Z'),
|
|
},
|
|
};
|
|
|
|
const dateHistogramLayer: LayerArgs = {
|
|
layerId: 'timeLayer',
|
|
hide: false,
|
|
xAccessor: 'xAccessorId',
|
|
yScaleType: 'linear',
|
|
xScaleType: 'time',
|
|
isHistogram: true,
|
|
splitAccessor: 'splitAccessorId',
|
|
seriesType: 'bar_stacked',
|
|
accessors: ['yAccessorId'],
|
|
palette: mockPaletteOutput,
|
|
};
|
|
|
|
const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable => ({
|
|
type: 'datatable',
|
|
columns: [
|
|
{
|
|
id: 'a',
|
|
name: 'a',
|
|
meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } },
|
|
},
|
|
{
|
|
id: 'b',
|
|
name: 'b',
|
|
meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } },
|
|
},
|
|
{
|
|
id: 'c',
|
|
name: 'c',
|
|
meta: {
|
|
type: 'date',
|
|
field: 'order_date',
|
|
sourceParams: { type: 'date-histogram', params: { interval: 'auto' } },
|
|
params: { id: 'string' },
|
|
},
|
|
},
|
|
{ id: 'd', name: 'ColD', meta: { type: 'string' } },
|
|
],
|
|
rows,
|
|
});
|
|
|
|
const sampleLayer: LayerArgs = {
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'c',
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
|
xScaleType: 'ordinal',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
};
|
|
|
|
const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({
|
|
xTitle: '',
|
|
yTitle: '',
|
|
yRightTitle: '',
|
|
legend: {
|
|
type: 'lens_xy_legendConfig',
|
|
isVisible: false,
|
|
position: Position.Top,
|
|
},
|
|
valueLabels: 'hide',
|
|
axisTitlesVisibilitySettings: {
|
|
type: 'lens_xy_axisTitlesVisibilityConfig',
|
|
x: true,
|
|
yLeft: true,
|
|
yRight: true,
|
|
},
|
|
tickLabelsVisibilitySettings: {
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
gridlinesVisibilitySettings: {
|
|
type: 'lens_xy_gridlinesConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
layers,
|
|
});
|
|
|
|
function sampleArgs() {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: createSampleDatatableWithRows([
|
|
{ a: 1, b: 2, c: 'I', d: 'Foo' },
|
|
{ a: 1, b: 5, c: 'J', d: 'Bar' },
|
|
]),
|
|
},
|
|
dateRange: {
|
|
fromDate: new Date('2019-01-02T05:00:00.000Z'),
|
|
toDate: new Date('2019-01-03T05:00:00.000Z'),
|
|
},
|
|
};
|
|
|
|
const args: XYArgs = createArgsWithLayers();
|
|
|
|
return { data, args };
|
|
}
|
|
|
|
describe('xy_expression', () => {
|
|
describe('configs', () => {
|
|
test('legendConfig produces the correct arguments', () => {
|
|
const args: LegendConfig = {
|
|
isVisible: true,
|
|
position: Position.Left,
|
|
};
|
|
|
|
const result = legendConfig.fn(null, args, createMockExecutionContext());
|
|
|
|
expect(result).toEqual({
|
|
type: 'lens_xy_legendConfig',
|
|
...args,
|
|
});
|
|
});
|
|
|
|
test('layerConfig produces the correct arguments', () => {
|
|
const args: LayerArgs = {
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'c',
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: 'd',
|
|
xScaleType: 'linear',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
};
|
|
|
|
const result = layerConfig.fn(null, args, createMockExecutionContext());
|
|
|
|
expect(result).toEqual({
|
|
type: 'lens_xy_layer',
|
|
...args,
|
|
});
|
|
});
|
|
});
|
|
|
|
test('tickLabelsConfig produces the correct arguments', () => {
|
|
const args: AxesSettingsConfig = {
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
};
|
|
|
|
const result = tickLabelsConfig.fn(null, args, createMockExecutionContext());
|
|
|
|
expect(result).toEqual({
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
...args,
|
|
});
|
|
});
|
|
|
|
test('gridlinesConfig produces the correct arguments', () => {
|
|
const args: AxesSettingsConfig = {
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
};
|
|
|
|
const result = gridlinesConfig.fn(null, args, createMockExecutionContext());
|
|
|
|
expect(result).toEqual({
|
|
type: 'lens_xy_gridlinesConfig',
|
|
...args,
|
|
});
|
|
});
|
|
|
|
describe('xyChart', () => {
|
|
test('it renders with the specified data and args', () => {
|
|
const { data, args } = sampleArgs();
|
|
const result = xyChart.fn(data, args, createMockExecutionContext());
|
|
|
|
expect(result).toEqual({
|
|
type: 'render',
|
|
as: 'lens_xy_chart_renderer',
|
|
value: { data, args },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('XYChart component', () => {
|
|
let getFormatSpy: jest.Mock;
|
|
let convertSpy: jest.Mock;
|
|
let defaultProps: Omit<XYChartRenderProps, 'data' | 'args'>;
|
|
|
|
const dataWithoutFormats: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'string' } },
|
|
{ id: 'd', name: 'd', meta: { type: 'string' } },
|
|
],
|
|
rows: [
|
|
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
|
|
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
const dataWithFormats: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'string' } },
|
|
{ id: 'd', name: 'd', meta: { type: 'string', params: { id: 'custom' } } },
|
|
],
|
|
rows: [
|
|
{ a: 1, b: 2, c: 'I', d: 'Row 1' },
|
|
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => {
|
|
return shallow(<XYChart {...defaultProps} data={data} args={args} />);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
convertSpy = jest.fn((x) => x);
|
|
getFormatSpy = jest.fn();
|
|
getFormatSpy.mockReturnValue({ convert: convertSpy });
|
|
|
|
defaultProps = {
|
|
formatFactory: getFormatSpy,
|
|
timeZone: 'UTC',
|
|
renderMode: 'display',
|
|
chartsThemeService,
|
|
paletteService,
|
|
minInterval: 50,
|
|
onClickValue,
|
|
onSelectRange,
|
|
syncColors: false,
|
|
};
|
|
});
|
|
|
|
test('it renders line', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(LineSeries)).toHaveLength(2);
|
|
expect(component.find(LineSeries).at(0).prop('yAccessors')).toEqual(['a']);
|
|
expect(component.find(LineSeries).at(1).prop('yAccessors')).toEqual(['b']);
|
|
});
|
|
|
|
describe('date range', () => {
|
|
const timeSampleLayer: LayerArgs = {
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'c',
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
|
xScaleType: 'time',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
};
|
|
const multiLayerArgs = createArgsWithLayers([
|
|
timeSampleLayer,
|
|
{
|
|
...timeSampleLayer,
|
|
layerId: 'second',
|
|
seriesType: 'bar',
|
|
xScaleType: 'time',
|
|
},
|
|
]);
|
|
test('it uses the full date range', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{
|
|
...data,
|
|
dateRange: {
|
|
fromDate: new Date('2019-01-02T05:00:00.000Z'),
|
|
toDate: new Date('2019-01-03T05:00:00.000Z'),
|
|
},
|
|
}}
|
|
args={{
|
|
...args,
|
|
layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'time' }],
|
|
}}
|
|
minInterval={undefined}
|
|
/>
|
|
);
|
|
expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(`
|
|
Object {
|
|
"max": 1546491600000,
|
|
"min": 1546405200000,
|
|
"minInterval": undefined,
|
|
}
|
|
`);
|
|
});
|
|
|
|
test('it uses passed in minInterval', () => {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]),
|
|
second: createSampleDatatableWithRows([]),
|
|
},
|
|
};
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{
|
|
...data,
|
|
dateRange: {
|
|
fromDate: new Date('2019-01-02T05:00:00.000Z'),
|
|
toDate: new Date('2019-01-03T05:00:00.000Z'),
|
|
},
|
|
}}
|
|
args={multiLayerArgs}
|
|
/>
|
|
);
|
|
|
|
// real auto interval is 30mins = 1800000
|
|
expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(`
|
|
Object {
|
|
"max": 1546491600000,
|
|
"min": 1546405200000,
|
|
"minInterval": 50,
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
test('it does not use date range if the x is not a time scale', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{
|
|
...data,
|
|
dateRange: {
|
|
fromDate: new Date('2019-01-02T05:00:00.000Z'),
|
|
toDate: new Date('2019-01-03T05:00:00.000Z'),
|
|
},
|
|
}}
|
|
args={{
|
|
...args,
|
|
layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }],
|
|
}}
|
|
/>
|
|
);
|
|
expect(component.find(Settings).prop('xDomain')).toBeUndefined();
|
|
});
|
|
|
|
test('it renders bar', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(BarSeries)).toHaveLength(2);
|
|
expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']);
|
|
expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']);
|
|
});
|
|
|
|
test('it renders area', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(AreaSeries)).toHaveLength(2);
|
|
expect(component.find(AreaSeries).at(0).prop('yAccessors')).toEqual(['a']);
|
|
expect(component.find(AreaSeries).at(1).prop('yAccessors')).toEqual(['b']);
|
|
});
|
|
|
|
test('it renders horizontal bar', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(BarSeries)).toHaveLength(2);
|
|
expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']);
|
|
expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']);
|
|
expect(component.find(Settings).prop('rotation')).toEqual(90);
|
|
});
|
|
|
|
test('it renders regular bar empty placeholder for no results', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
// send empty data to the chart
|
|
data.tables.first.rows = [];
|
|
|
|
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
|
|
|
|
expect(component.find(BarSeries)).toHaveLength(0);
|
|
expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined();
|
|
});
|
|
|
|
test('onBrushEnd returns correct context data for date histogram data', () => {
|
|
const { args } = sampleArgs();
|
|
|
|
const wrapper = mountWithIntl(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={dateHistogramData}
|
|
args={{
|
|
...args,
|
|
layers: [dateHistogramLayer],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [1585757732783, 1585758880838] });
|
|
|
|
expect(onSelectRange).toHaveBeenCalledWith({
|
|
column: 0,
|
|
table: dateHistogramData.tables.timeLayer,
|
|
range: [1585757732783, 1585758880838],
|
|
timeFieldName: 'order_date',
|
|
});
|
|
});
|
|
|
|
test('onBrushEnd returns correct context data for number histogram data', () => {
|
|
const { args } = sampleArgs();
|
|
|
|
const numberLayer: LayerArgs = {
|
|
layerId: 'numberLayer',
|
|
hide: false,
|
|
xAccessor: 'xAccessorId',
|
|
yScaleType: 'linear',
|
|
xScaleType: 'linear',
|
|
isHistogram: true,
|
|
seriesType: 'bar_stacked',
|
|
accessors: ['yAccessorId'],
|
|
palette: mockPaletteOutput,
|
|
};
|
|
|
|
const numberHistogramData: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
numberLayer: {
|
|
type: 'datatable',
|
|
rows: [
|
|
{
|
|
xAccessorId: 5,
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 7,
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 8,
|
|
yAccessorId: 1,
|
|
},
|
|
{
|
|
xAccessorId: 10,
|
|
yAccessorId: 1,
|
|
},
|
|
],
|
|
columns: [
|
|
{
|
|
id: 'xAccessorId',
|
|
name: 'bytes',
|
|
meta: { type: 'number' },
|
|
},
|
|
{
|
|
id: 'yAccessorId',
|
|
name: 'Count of records',
|
|
meta: { type: 'number' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
dateRange: {
|
|
fromDate: new Date('2020-04-01T16:14:16.246Z'),
|
|
toDate: new Date('2020-04-01T17:15:41.263Z'),
|
|
},
|
|
};
|
|
|
|
const wrapper = mountWithIntl(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={numberHistogramData}
|
|
args={{
|
|
...args,
|
|
layers: [numberLayer],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [5, 8] });
|
|
|
|
expect(onSelectRange).toHaveBeenCalledWith({
|
|
column: 0,
|
|
table: numberHistogramData.tables.numberLayer,
|
|
range: [5, 8],
|
|
timeFieldName: undefined,
|
|
});
|
|
});
|
|
|
|
test('onBrushEnd is not set on noInteractivity mode', () => {
|
|
const { args, data } = sampleArgs();
|
|
|
|
const wrapper = mountWithIntl(
|
|
<XYChart {...defaultProps} data={data} args={args} renderMode="noInteractivity" />
|
|
);
|
|
|
|
expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
|
|
});
|
|
|
|
test('onElementClick returns correct context data', () => {
|
|
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} };
|
|
const series = {
|
|
key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
|
|
specId: 'd',
|
|
yAccessor: 'd',
|
|
splitAccessors: {},
|
|
seriesKeys: [2, 'd'],
|
|
};
|
|
|
|
const { args, data } = sampleArgs();
|
|
|
|
const wrapper = mountWithIntl(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{
|
|
...args,
|
|
layers: [
|
|
{
|
|
layerId: 'first',
|
|
isHistogram: true,
|
|
seriesType: 'bar_stacked',
|
|
xAccessor: 'b',
|
|
yScaleType: 'linear',
|
|
xScaleType: 'time',
|
|
splitAccessor: 'b',
|
|
accessors: ['d'],
|
|
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
|
palette: mockPaletteOutput,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
wrapper.find(Settings).first().prop('onElementClick')!([
|
|
[geometry, series as XYChartSeriesIdentifier],
|
|
]);
|
|
|
|
expect(onClickValue).toHaveBeenCalledWith({
|
|
data: [
|
|
{
|
|
column: 1,
|
|
row: 1,
|
|
table: data.tables.first,
|
|
value: 5,
|
|
},
|
|
{
|
|
column: 1,
|
|
row: 0,
|
|
table: data.tables.first,
|
|
value: 2,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('onElementClick is not triggering event on noInteractivity mode', () => {
|
|
const { args, data } = sampleArgs();
|
|
|
|
const wrapper = mountWithIntl(
|
|
<XYChart {...defaultProps} data={data} args={args} renderMode="noInteractivity" />
|
|
);
|
|
|
|
expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined();
|
|
});
|
|
|
|
test('it renders stacked bar', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(BarSeries)).toHaveLength(2);
|
|
expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1);
|
|
expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1);
|
|
});
|
|
|
|
test('it renders stacked area', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(AreaSeries)).toHaveLength(2);
|
|
expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toHaveLength(1);
|
|
expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toHaveLength(1);
|
|
});
|
|
|
|
test('it renders stacked horizontal bar', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{
|
|
...args,
|
|
layers: [{ ...args.layers[0], seriesType: 'bar_horizontal_stacked' }],
|
|
}}
|
|
/>
|
|
);
|
|
expect(component).toMatchSnapshot();
|
|
expect(component.find(BarSeries)).toHaveLength(2);
|
|
expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1);
|
|
expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1);
|
|
expect(component.find(Settings).prop('rotation')).toEqual(90);
|
|
});
|
|
|
|
test('it renders stacked bar empty placeholder for no results', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
xAccessor: undefined,
|
|
splitAccessor: 'e',
|
|
seriesType: 'bar_stacked',
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
expect(component.find(BarSeries)).toHaveLength(0);
|
|
expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined();
|
|
});
|
|
|
|
test('it passes time zone to the series', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={data} args={args} timeZone="CEST" />
|
|
);
|
|
expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST');
|
|
expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST');
|
|
});
|
|
|
|
test('it applies histogram mode to the series for single series', () => {
|
|
const { data, args } = sampleArgs();
|
|
const firstLayer: LayerArgs = {
|
|
...args.layers[0],
|
|
accessors: ['b'],
|
|
seriesType: 'bar',
|
|
isHistogram: true,
|
|
};
|
|
delete firstLayer.splitAccessor;
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={data} args={{ ...args, layers: [firstLayer] }} />
|
|
);
|
|
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
|
|
});
|
|
|
|
test('it does not apply histogram mode to more than one bar series for unstacked bar chart', () => {
|
|
const { data, args } = sampleArgs();
|
|
const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true };
|
|
delete firstLayer.splitAccessor;
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={data} args={{ ...args, layers: [firstLayer] }} />
|
|
);
|
|
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false);
|
|
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false);
|
|
});
|
|
|
|
test('it applies histogram mode to more than one the series for unstacked line/area chart', () => {
|
|
const { data, args } = sampleArgs();
|
|
const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'line', isHistogram: true };
|
|
delete firstLayer.splitAccessor;
|
|
const secondLayer: LayerArgs = { ...args.layers[0], seriesType: 'line', isHistogram: true };
|
|
delete secondLayer.splitAccessor;
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [firstLayer, secondLayer] }}
|
|
/>
|
|
);
|
|
expect(component.find(LineSeries).at(0).prop('enableHistogramMode')).toEqual(true);
|
|
expect(component.find(LineSeries).at(1).prop('enableHistogramMode')).toEqual(true);
|
|
});
|
|
|
|
test('it applies histogram mode to the series for stacked series', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
seriesType: 'bar_stacked',
|
|
isHistogram: true,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
);
|
|
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
|
|
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true);
|
|
});
|
|
|
|
test('it does not apply histogram mode for splitted series', () => {
|
|
const { data, args } = sampleArgs();
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{
|
|
...args,
|
|
layers: [{ ...args.layers[0], seriesType: 'bar', isHistogram: true }],
|
|
}}
|
|
/>
|
|
);
|
|
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false);
|
|
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false);
|
|
});
|
|
|
|
describe('y axes', () => {
|
|
test('single axis if possible', () => {
|
|
const args = createArgsWithLayers();
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, args);
|
|
const axes = component.find(Axis);
|
|
expect(axes).toHaveLength(2);
|
|
});
|
|
|
|
test('multiple axes because of config', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a', 'b'],
|
|
yConfig: [
|
|
{
|
|
forAccessor: 'a',
|
|
axisMode: 'left',
|
|
},
|
|
{
|
|
forAccessor: 'b',
|
|
axisMode: 'right',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as XYArgs;
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const axes = component.find(Axis);
|
|
expect(axes).toHaveLength(3);
|
|
expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(
|
|
axes.at(1).prop('groupId')
|
|
);
|
|
expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(
|
|
axes.at(2).prop('groupId')
|
|
);
|
|
});
|
|
|
|
test('multiple axes because of incompatible formatters', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['c', 'd'],
|
|
},
|
|
],
|
|
} as XYArgs;
|
|
|
|
const component = getRenderedComponent(dataWithFormats, newArgs);
|
|
const axes = component.find(Axis);
|
|
expect(axes).toHaveLength(3);
|
|
expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(
|
|
axes.at(1).prop('groupId')
|
|
);
|
|
expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(
|
|
axes.at(2).prop('groupId')
|
|
);
|
|
});
|
|
|
|
test('single axis despite different formatters if enforced', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['c', 'd'],
|
|
yConfig: [
|
|
{
|
|
forAccessor: 'c',
|
|
axisMode: 'left',
|
|
},
|
|
{
|
|
forAccessor: 'd',
|
|
axisMode: 'left',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as XYArgs;
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const axes = component.find(Axis);
|
|
expect(axes).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('y series coloring', () => {
|
|
test('color is applied to chart for multiple series', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
splitAccessor: undefined,
|
|
accessors: ['a', 'b'],
|
|
yConfig: [
|
|
{
|
|
forAccessor: 'a',
|
|
color: '#550000',
|
|
},
|
|
{
|
|
forAccessor: 'b',
|
|
color: '#FFFF00',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
...args.layers[0],
|
|
splitAccessor: undefined,
|
|
accessors: ['c'],
|
|
yConfig: [
|
|
{
|
|
forAccessor: 'c',
|
|
color: '#FEECDF',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
} as XYArgs;
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
expect(
|
|
(component.find(LineSeries).at(0).prop('color') as Function)!({
|
|
yAccessor: 'a',
|
|
seriesKeys: ['a'],
|
|
})
|
|
).toEqual('#550000');
|
|
expect(
|
|
(component.find(LineSeries).at(1).prop('color') as Function)!({
|
|
yAccessor: 'b',
|
|
seriesKeys: ['b'],
|
|
})
|
|
).toEqual('#FFFF00');
|
|
expect(
|
|
(component.find(LineSeries).at(2).prop('color') as Function)!({
|
|
yAccessor: 'c',
|
|
seriesKeys: ['c'],
|
|
})
|
|
).toEqual('#FEECDF');
|
|
});
|
|
test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
yConfig: [
|
|
{
|
|
forAccessor: 'a',
|
|
color: '#550000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
...args.layers[0],
|
|
splitAccessor: undefined,
|
|
accessors: ['c'],
|
|
},
|
|
],
|
|
} as XYArgs;
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
expect(
|
|
(component.find(LineSeries).at(0).prop('color') as Function)!({
|
|
yAccessor: 'a',
|
|
seriesKeys: ['a'],
|
|
})
|
|
).toEqual('blue');
|
|
expect(
|
|
(component.find(LineSeries).at(1).prop('color') as Function)!({
|
|
yAccessor: 'c',
|
|
seriesKeys: ['c'],
|
|
})
|
|
).toEqual('blue');
|
|
});
|
|
});
|
|
|
|
describe('provides correct series naming', () => {
|
|
const nameFnArgs = {
|
|
seriesKeys: [],
|
|
key: '',
|
|
specId: 'a',
|
|
yAccessor: '',
|
|
splitAccessors: new Map(),
|
|
};
|
|
|
|
test('simplest xy chart without human-readable name', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
splitAccessor: undefined,
|
|
columnToLabel: '',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn = 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('');
|
|
expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual('');
|
|
});
|
|
|
|
test('simplest xy chart with empty name', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
splitAccessor: undefined,
|
|
columnToLabel: '{"a":""}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn = 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('');
|
|
expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual('');
|
|
});
|
|
|
|
test('simplest xy chart with human-readable name', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
splitAccessor: undefined,
|
|
columnToLabel: '{"a":"Column A"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
|
|
|
|
expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A');
|
|
});
|
|
|
|
test('multiple y accessors', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: undefined,
|
|
columnToLabel: '{"a": "Label A"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
|
|
const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn;
|
|
|
|
// 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('');
|
|
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual('');
|
|
});
|
|
|
|
test('split series without formatting and single y accessor', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
|
|
|
|
expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1');
|
|
});
|
|
|
|
test('split series with formatting and single y accessor', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithFormats, newArgs);
|
|
const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn;
|
|
|
|
convertSpy.mockReturnValueOnce('formatted');
|
|
expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted');
|
|
expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' });
|
|
});
|
|
|
|
test('split series without formatting with multiple y accessors', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A","b": "Label B"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithoutFormats, newArgs);
|
|
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
|
|
const nameFn2 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
|
|
|
|
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual(
|
|
'split1 - Label A'
|
|
);
|
|
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
|
|
'split1 - Label B'
|
|
);
|
|
});
|
|
|
|
test('split series with formatting with multiple y accessors', () => {
|
|
const args = createArgsWithLayers();
|
|
const newArgs = {
|
|
...args,
|
|
layers: [
|
|
{
|
|
...args.layers[0],
|
|
accessors: ['a', 'b'],
|
|
splitAccessor: 'd',
|
|
columnToLabel: '{"a": "Label A","b": "Label B"}',
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = getRenderedComponent(dataWithFormats, newArgs);
|
|
const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn;
|
|
const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn;
|
|
|
|
convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2');
|
|
expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual(
|
|
'formatted1 - Label A'
|
|
);
|
|
expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual(
|
|
'formatted2 - Label B'
|
|
);
|
|
});
|
|
});
|
|
|
|
test('it set the scale of the x axis according to the args prop', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }}
|
|
/>
|
|
);
|
|
expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal);
|
|
expect(component.find(LineSeries).at(1).prop('xScaleType')).toEqual(ScaleType.Ordinal);
|
|
});
|
|
|
|
test('it set the scale of the y axis according to the args prop', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={data}
|
|
args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }}
|
|
/>
|
|
);
|
|
expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt);
|
|
expect(component.find(LineSeries).at(1).prop('yScaleType')).toEqual(ScaleType.Sqrt);
|
|
});
|
|
|
|
test('it gets the formatter for the x axis', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
|
|
|
|
expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' });
|
|
});
|
|
|
|
test('it gets the formatter for the y axis if there is only one accessor', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{ ...data }}
|
|
args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }}
|
|
/>
|
|
);
|
|
expect(getFormatSpy).toHaveBeenCalledWith({
|
|
id: 'number',
|
|
params: { pattern: '0,0.000' },
|
|
});
|
|
});
|
|
|
|
test('it should pass the formatter function to the axis', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} 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');
|
|
});
|
|
|
|
test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.tickLabelsVisibilitySettings = {
|
|
x: false,
|
|
yLeft: true,
|
|
yRight: true,
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
};
|
|
|
|
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
|
|
|
|
const axisStyle = instance.find(Axis).first().prop('style');
|
|
|
|
expect(axisStyle).toMatchObject({
|
|
tickLabel: {
|
|
visible: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.tickLabelsVisibilitySettings = {
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
};
|
|
|
|
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
|
|
|
|
const axisStyle = instance.find(Axis).at(1).prop('style');
|
|
|
|
expect(axisStyle).toMatchObject({
|
|
tickLabel: {
|
|
visible: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.tickLabelsVisibilitySettings = {
|
|
x: true,
|
|
yLeft: true,
|
|
yRight: true,
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
};
|
|
|
|
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
|
|
|
|
const axisStyle = instance.find(Axis).first().prop('style');
|
|
|
|
expect(axisStyle).toMatchObject({
|
|
tickLabel: {
|
|
visible: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.tickLabelsVisibilitySettings = {
|
|
x: false,
|
|
yLeft: true,
|
|
yRight: true,
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
};
|
|
|
|
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
|
|
|
|
const axisStyle = instance.find(Axis).at(1).prop('style');
|
|
|
|
expect(axisStyle).toMatchObject({
|
|
tickLabel: {
|
|
visible: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('it should remove invalid rows', () => {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'string' } },
|
|
],
|
|
rows: [
|
|
{ a: undefined, b: 2, c: 'I', d: 'Row 1' },
|
|
{ a: 1, b: 5, c: 'J', d: 'Row 2' },
|
|
],
|
|
},
|
|
second: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'string' } },
|
|
],
|
|
rows: [
|
|
{ a: undefined, b: undefined, c: undefined },
|
|
{ a: undefined, b: undefined, c: undefined },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const args: XYArgs = {
|
|
xTitle: '',
|
|
yTitle: '',
|
|
yRightTitle: '',
|
|
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
|
|
valueLabels: 'hide',
|
|
tickLabelsVisibilitySettings: {
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
x: true,
|
|
yLeft: true,
|
|
yRight: true,
|
|
},
|
|
gridlinesVisibilitySettings: {
|
|
type: 'lens_xy_gridlinesConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
layers: [
|
|
{
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'a',
|
|
accessors: ['c'],
|
|
splitAccessor: 'b',
|
|
columnToLabel: '',
|
|
xScaleType: 'ordinal',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
},
|
|
{
|
|
layerId: 'second',
|
|
seriesType: 'line',
|
|
xAccessor: 'a',
|
|
accessors: ['c'],
|
|
splitAccessor: 'b',
|
|
columnToLabel: '',
|
|
xScaleType: 'ordinal',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
|
|
|
|
const series = component.find(LineSeries);
|
|
|
|
// Only one series should be rendered, even though 2 are configured
|
|
// This one series should only have one row, even though 2 are sent
|
|
expect(series.prop('data')).toEqual([{ a: 1, b: 5, c: 'J', d: 'Row 2' }]);
|
|
});
|
|
|
|
test('it should not remove rows with falsy but non-undefined values', () => {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'number' } },
|
|
],
|
|
rows: [
|
|
{ a: 0, b: 2, c: 5 },
|
|
{ a: 1, b: 0, c: 7 },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const args: XYArgs = {
|
|
xTitle: '',
|
|
yTitle: '',
|
|
yRightTitle: '',
|
|
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
|
|
valueLabels: 'hide',
|
|
tickLabelsVisibilitySettings: {
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
gridlinesVisibilitySettings: {
|
|
type: 'lens_xy_gridlinesConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
layers: [
|
|
{
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'a',
|
|
accessors: ['c'],
|
|
splitAccessor: 'b',
|
|
columnToLabel: '',
|
|
xScaleType: 'ordinal',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
|
|
|
|
const series = component.find(LineSeries);
|
|
|
|
expect(series.prop('data')).toEqual([
|
|
{ a: 0, b: 2, c: 5 },
|
|
{ a: 1, b: 0, c: 7 },
|
|
]);
|
|
});
|
|
|
|
test('it should show legend for split series, even with one row', () => {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: {
|
|
type: 'datatable',
|
|
columns: [
|
|
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
|
{ id: 'b', name: 'b', meta: { type: 'number' } },
|
|
{ id: 'c', name: 'c', meta: { type: 'string' } },
|
|
],
|
|
rows: [{ a: 1, b: 5, c: 'J' }],
|
|
},
|
|
},
|
|
};
|
|
|
|
const args: XYArgs = {
|
|
xTitle: '',
|
|
yTitle: '',
|
|
yRightTitle: '',
|
|
legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top },
|
|
valueLabels: 'hide',
|
|
tickLabelsVisibilitySettings: {
|
|
type: 'lens_xy_tickLabelsConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
gridlinesVisibilitySettings: {
|
|
type: 'lens_xy_gridlinesConfig',
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
},
|
|
layers: [
|
|
{
|
|
layerId: 'first',
|
|
seriesType: 'line',
|
|
xAccessor: 'a',
|
|
accessors: ['c'],
|
|
splitAccessor: 'b',
|
|
columnToLabel: '',
|
|
xScaleType: 'ordinal',
|
|
yScaleType: 'linear',
|
|
isHistogram: false,
|
|
palette: mockPaletteOutput,
|
|
},
|
|
],
|
|
};
|
|
|
|
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
|
|
|
|
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
|
});
|
|
|
|
test('it should always show legend if showSingleSeries is set', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{ ...data }}
|
|
args={{
|
|
...args,
|
|
layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }],
|
|
legend: { ...args.legend, isVisible: true, showSingleSeries: true },
|
|
}}
|
|
/>
|
|
);
|
|
|
|
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
|
});
|
|
|
|
test('it not show legend if isVisible is set to false', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{ ...data }}
|
|
args={{
|
|
...args,
|
|
legend: { ...args.legend, isVisible: false },
|
|
}}
|
|
/>
|
|
);
|
|
|
|
expect(component.find(Settings).prop('showLegend')).toEqual(false);
|
|
});
|
|
|
|
test('it should show legend on right side', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{ ...data }}
|
|
args={{
|
|
...args,
|
|
legend: { ...args.legend, position: 'top' },
|
|
}}
|
|
/>
|
|
);
|
|
|
|
expect(component.find(Settings).prop('legendPosition')).toEqual('top');
|
|
});
|
|
|
|
test('it should apply the fitting function to all non-bar series', () => {
|
|
const data: LensMultiTable = {
|
|
type: 'lens_multitable',
|
|
tables: {
|
|
first: createSampleDatatableWithRows([
|
|
{ a: 1, b: 2, c: 'I', d: 'Foo' },
|
|
{ a: 1, b: 5, c: 'J', d: 'Bar' },
|
|
]),
|
|
},
|
|
};
|
|
|
|
const args: XYArgs = createArgsWithLayers([
|
|
{ ...sampleLayer, accessors: ['a'] },
|
|
{ ...sampleLayer, seriesType: 'bar', accessors: ['a'] },
|
|
{ ...sampleLayer, seriesType: 'area', accessors: ['a'] },
|
|
{ ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] },
|
|
]);
|
|
|
|
const component = shallow(
|
|
<XYChart
|
|
{...defaultProps}
|
|
data={{ ...data }}
|
|
args={{ ...args, fittingFunction: 'Carry' }}
|
|
/>
|
|
);
|
|
|
|
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.Carry });
|
|
expect(component.find(BarSeries).prop('fit')).toEqual(undefined);
|
|
expect(component.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry });
|
|
expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]);
|
|
expect(component.find(AreaSeries).at(1).prop('fit')).toEqual({ type: Fit.Carry });
|
|
expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']);
|
|
});
|
|
|
|
test('it should apply None fitting function if not specified', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.layers[0].accessors = ['a'];
|
|
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
|
|
);
|
|
|
|
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None });
|
|
});
|
|
|
|
test('it should apply the xTitle if is specified', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.xTitle = 'My custom x-axis title';
|
|
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
|
|
);
|
|
|
|
expect(component.find(Axis).at(0).prop('title')).toEqual('My custom x-axis title');
|
|
});
|
|
|
|
test('it should hide the X axis title if the corresponding switch is off', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.axisTitlesVisibilitySettings = {
|
|
x: false,
|
|
yLeft: true,
|
|
yRight: true,
|
|
type: 'lens_xy_axisTitlesVisibilityConfig',
|
|
};
|
|
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
|
|
);
|
|
|
|
const axisStyle = component.find(Axis).first().prop('style');
|
|
|
|
expect(axisStyle).toMatchObject({
|
|
axisTitle: {
|
|
visible: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('it should show the X axis gridlines if the setting is on', () => {
|
|
const { data, args } = sampleArgs();
|
|
|
|
args.gridlinesVisibilitySettings = {
|
|
x: true,
|
|
yLeft: false,
|
|
yRight: false,
|
|
type: 'lens_xy_gridlinesConfig',
|
|
};
|
|
|
|
const component = shallow(
|
|
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
|
|
);
|
|
|
|
expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({
|
|
visible: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('calculateMinInterval', () => {
|
|
let xyProps: XYChartProps;
|
|
|
|
beforeEach(() => {
|
|
xyProps = sampleArgs();
|
|
xyProps.args.layers[0].xScaleType = 'time';
|
|
});
|
|
it('should use first valid layer and determine interval', async () => {
|
|
const result = await calculateMinInterval(
|
|
xyProps,
|
|
jest.fn().mockResolvedValue({ interval: '5m' })
|
|
);
|
|
expect(result).toEqual(5 * 60 * 1000);
|
|
});
|
|
|
|
it('should return undefined if data table is empty', async () => {
|
|
xyProps.data.tables.first.rows = [];
|
|
const result = await calculateMinInterval(
|
|
xyProps,
|
|
jest.fn().mockResolvedValue({ interval: '5m' })
|
|
);
|
|
expect(result).toEqual(undefined);
|
|
});
|
|
|
|
it('should return undefined if interval can not be checked', async () => {
|
|
const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined));
|
|
expect(result).toEqual(undefined);
|
|
});
|
|
|
|
it('should return undefined if date column is not found', async () => {
|
|
xyProps.data.tables.first.columns.splice(2, 1);
|
|
const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined));
|
|
expect(result).toEqual(undefined);
|
|
});
|
|
|
|
it('should return undefined if x axis is not a date', async () => {
|
|
xyProps.args.layers[0].xScaleType = 'ordinal';
|
|
xyProps.data.tables.first.columns.splice(2, 1);
|
|
const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined));
|
|
expect(result).toEqual(undefined);
|
|
});
|
|
});
|
|
});
|