2019-09-17 20:57:53 +02:00
|
|
|
/*
|
|
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
2021-02-04 03:12:39 +01:00
|
|
|
* 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.
|
2019-09-17 20:57:53 +02:00
|
|
|
*/
|
|
|
|
|
2020-11-04 11:27:52 +01:00
|
|
|
import { getXyVisualization } from './visualization';
|
2019-09-17 20:57:53 +02:00
|
|
|
import { Position } from '@elastic/charts';
|
|
|
|
import { Operation } from '../types';
|
2021-02-03 12:03:55 +01:00
|
|
|
import { State, SeriesType, XYLayerConfig } from './types';
|
2020-02-13 13:54:51 +01:00
|
|
|
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
2020-09-15 11:24:02 +02:00
|
|
|
import { LensIconChartBar } from '../assets/chart_bar';
|
2020-11-04 11:27:52 +01:00
|
|
|
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
2020-11-17 10:19:13 +01:00
|
|
|
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
function exampleState(): State {
|
|
|
|
return {
|
|
|
|
legend: { position: Position.Bottom, isVisible: true },
|
2020-11-06 16:34:30 +01:00
|
|
|
valueLabels: 'hide',
|
2019-09-17 20:57:53 +02:00
|
|
|
preferredSeriesType: 'bar',
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
splitAccessor: 'd',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['b', 'c'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
2020-11-17 10:19:13 +01:00
|
|
|
const paletteServiceMock = chartPluginMock.createPaletteRegistry();
|
|
|
|
const dataMock = dataPluginMock.createStartContract();
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-11-04 11:27:52 +01:00
|
|
|
const xyVisualization = getXyVisualization({
|
2020-11-17 10:19:13 +01:00
|
|
|
paletteService: paletteServiceMock,
|
|
|
|
data: dataMock,
|
2020-11-04 11:27:52 +01:00
|
|
|
});
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
describe('xy_visualization', () => {
|
2020-04-30 01:14:47 +02:00
|
|
|
describe('#getDescription', () => {
|
2019-10-02 19:21:39 +02:00
|
|
|
function mixedState(...types: SeriesType[]) {
|
|
|
|
const state = exampleState();
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
layers: types.map((t, i) => ({
|
|
|
|
...state.layers[0],
|
|
|
|
layerId: `layer_${i}`,
|
|
|
|
seriesType: t,
|
|
|
|
})),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should show mixed xy chart when multilple series types', () => {
|
|
|
|
const desc = xyVisualization.getDescription(mixedState('bar', 'line'));
|
|
|
|
|
2020-10-06 17:17:41 +02:00
|
|
|
expect(desc.label).toEqual('Mixed XY');
|
2019-10-02 19:21:39 +02:00
|
|
|
});
|
|
|
|
|
2019-10-23 22:28:18 +02:00
|
|
|
it('should show the preferredSeriesType if there are no layers', () => {
|
|
|
|
const desc = xyVisualization.getDescription(mixedState());
|
|
|
|
|
2020-09-15 11:24:02 +02:00
|
|
|
expect(desc.icon).toEqual(LensIconChartBar);
|
2021-03-02 11:25:35 +01:00
|
|
|
expect(desc.label).toEqual('Bar vertical');
|
2019-10-23 22:28:18 +02:00
|
|
|
});
|
|
|
|
|
2019-10-02 19:21:39 +02:00
|
|
|
it('should show mixed horizontal bar chart when multiple horizontal bar types', () => {
|
|
|
|
const desc = xyVisualization.getDescription(
|
|
|
|
mixedState('bar_horizontal', 'bar_horizontal_stacked')
|
|
|
|
);
|
|
|
|
|
2021-03-02 11:25:35 +01:00
|
|
|
expect(desc.label).toEqual('Mixed bar horizontal');
|
2019-10-02 19:21:39 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should show bar chart when bar only', () => {
|
|
|
|
const desc = xyVisualization.getDescription(mixedState('bar_horizontal', 'bar_horizontal'));
|
|
|
|
|
2021-03-02 11:25:35 +01:00
|
|
|
expect(desc.label).toEqual('Bar horizontal');
|
2019-10-02 19:21:39 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should show the chart description if not mixed', () => {
|
2020-10-06 17:17:41 +02:00
|
|
|
expect(xyVisualization.getDescription(mixedState('area')).label).toEqual('Area');
|
|
|
|
expect(xyVisualization.getDescription(mixedState('line')).label).toEqual('Line');
|
2019-10-02 19:21:39 +02:00
|
|
|
expect(xyVisualization.getDescription(mixedState('area_stacked')).label).toEqual(
|
2021-03-02 11:25:35 +01:00
|
|
|
'Area stacked'
|
2019-10-02 19:21:39 +02:00
|
|
|
);
|
|
|
|
expect(xyVisualization.getDescription(mixedState('bar_horizontal_stacked')).label).toEqual(
|
2021-03-02 11:25:35 +01:00
|
|
|
'Bar horizontal stacked'
|
2019-10-02 19:21:39 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-04-30 01:14:47 +02:00
|
|
|
describe('#getVisualizationTypeId', () => {
|
|
|
|
function mixedState(...types: SeriesType[]) {
|
|
|
|
const state = exampleState();
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
layers: types.map((t, i) => ({
|
|
|
|
...state.layers[0],
|
|
|
|
layerId: `layer_${i}`,
|
|
|
|
seriesType: t,
|
|
|
|
})),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should show mixed when each layer is different', () => {
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should show the preferredSeriesType if there are no layers', () => {
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should combine multiple layers into one type', () => {
|
|
|
|
expect(
|
|
|
|
xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal'))
|
|
|
|
).toEqual('bar_horizontal');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return the subtype for single layers', () => {
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area');
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line');
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual(
|
|
|
|
'area_stacked'
|
|
|
|
);
|
|
|
|
expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual(
|
|
|
|
'bar_horizontal_stacked'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
describe('#initialize', () => {
|
|
|
|
it('loads default state', () => {
|
|
|
|
const mockFrame = createMockFramePublicAPI();
|
|
|
|
const initialState = xyVisualization.initialize(mockFrame);
|
|
|
|
|
|
|
|
expect(initialState.layers).toHaveLength(1);
|
2020-03-17 14:57:52 +01:00
|
|
|
expect(initialState.layers[0].xAccessor).not.toBeDefined();
|
|
|
|
expect(initialState.layers[0].accessors).toHaveLength(0);
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
expect(initialState).toMatchInlineSnapshot(`
|
|
|
|
Object {
|
|
|
|
"layers": Array [
|
|
|
|
Object {
|
2020-03-17 14:57:52 +01:00
|
|
|
"accessors": Array [],
|
2019-09-17 20:57:53 +02:00
|
|
|
"layerId": "",
|
|
|
|
"position": "top",
|
2019-10-01 21:04:00 +02:00
|
|
|
"seriesType": "bar_stacked",
|
2019-09-17 20:57:53 +02:00
|
|
|
"showGridlines": false,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
"legend": Object {
|
|
|
|
"isVisible": true,
|
|
|
|
"position": "right",
|
|
|
|
},
|
2019-10-01 21:04:00 +02:00
|
|
|
"preferredSeriesType": "bar_stacked",
|
2019-10-25 18:09:41 +02:00
|
|
|
"title": "Empty XY chart",
|
2020-11-06 16:34:30 +01:00
|
|
|
"valueLabels": "hide",
|
2019-09-17 20:57:53 +02:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('loads from persisted state', () => {
|
|
|
|
expect(xyVisualization.initialize(createMockFramePublicAPI(), exampleState())).toEqual(
|
|
|
|
exampleState()
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-01-14 18:06:51 +01:00
|
|
|
describe('#removeLayer', () => {
|
|
|
|
it('removes the specified layer', () => {
|
|
|
|
const prevState: State = {
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
...exampleState().layers,
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
splitAccessor: 'e',
|
|
|
|
xAccessor: 'f',
|
|
|
|
accessors: ['g', 'h'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
|
|
|
expect(xyVisualization.removeLayer!(prevState, 'second')).toEqual(exampleState());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#appendLayer', () => {
|
|
|
|
it('adds a layer', () => {
|
|
|
|
const layers = xyVisualization.appendLayer!(exampleState(), 'foo').layers;
|
|
|
|
expect(layers.length).toEqual(exampleState().layers.length + 1);
|
|
|
|
expect(layers[layers.length - 1]).toMatchObject({ layerId: 'foo' });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#clearLayer', () => {
|
|
|
|
it('clears the specified layer', () => {
|
|
|
|
const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0];
|
|
|
|
expect(layer).toMatchObject({
|
2020-03-17 14:57:52 +01:00
|
|
|
accessors: [],
|
2020-01-14 18:06:51 +01:00
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'bar',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#getLayerIds', () => {
|
|
|
|
it('returns layerids', () => {
|
|
|
|
expect(xyVisualization.getLayerIds(exampleState())).toEqual(['first']);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
describe('#setDimension', () => {
|
|
|
|
it('sets the x axis', () => {
|
|
|
|
expect(
|
|
|
|
xyVisualization.setDimension({
|
|
|
|
prevState: {
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
layerId: 'first',
|
|
|
|
groupId: 'x',
|
|
|
|
columnId: 'newCol',
|
|
|
|
}).layers[0]
|
|
|
|
).toEqual({
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'newCol',
|
|
|
|
accessors: [],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('replaces the x axis', () => {
|
|
|
|
expect(
|
|
|
|
xyVisualization.setDimension({
|
|
|
|
prevState: {
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
layerId: 'first',
|
|
|
|
groupId: 'x',
|
|
|
|
columnId: 'newCol',
|
|
|
|
}).layers[0]
|
|
|
|
).toEqual({
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'newCol',
|
|
|
|
accessors: [],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#removeDimension', () => {
|
|
|
|
it('removes the x axis', () => {
|
|
|
|
expect(
|
|
|
|
xyVisualization.removeDimension({
|
|
|
|
prevState: {
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
layerId: 'first',
|
|
|
|
columnId: 'a',
|
|
|
|
}).layers[0]
|
|
|
|
).toEqual({
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#getConfiguration', () => {
|
2019-09-17 20:57:53 +02:00
|
|
|
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
|
|
|
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
frame = createMockFramePublicAPI();
|
2020-03-17 14:57:52 +01:00
|
|
|
mockDatasource = createMockDatasource('testDatasource');
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([
|
|
|
|
{ columnId: 'd' },
|
|
|
|
{ columnId: 'a' },
|
|
|
|
{ columnId: 'b' },
|
|
|
|
{ columnId: 'c' },
|
|
|
|
]);
|
|
|
|
|
|
|
|
frame.datasourceLayers = {
|
|
|
|
first: mockDatasource.publicAPIMock,
|
|
|
|
};
|
2020-11-17 10:19:13 +01:00
|
|
|
|
|
|
|
frame.activeData = {
|
|
|
|
first: {
|
|
|
|
type: 'datatable',
|
|
|
|
rows: [],
|
|
|
|
columns: [],
|
|
|
|
},
|
|
|
|
};
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
it('should return options for 3 dimensions', () => {
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: exampleState(),
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
|
|
|
expect(options).toHaveLength(3);
|
2020-05-22 09:08:58 +02:00
|
|
|
expect(options.map((o) => o.groupId)).toEqual(['x', 'y', 'breakdown']);
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|
|
|
|
|
2020-10-22 12:13:05 +02:00
|
|
|
it('should return the correct labels for the 3 dimensios', () => {
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: exampleState(),
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
|
|
|
expect(options.map((o) => o.groupLabel)).toEqual([
|
|
|
|
'Horizontal axis',
|
|
|
|
'Vertical axis',
|
|
|
|
'Break down by',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return the correct labels for the 3 dimensios for a horizontal chart', () => {
|
|
|
|
const initialState = exampleState();
|
|
|
|
const state = {
|
|
|
|
...initialState,
|
|
|
|
layers: [{ ...initialState.layers[0], seriesType: 'bar_horizontal' as SeriesType }],
|
|
|
|
};
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state,
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
|
|
|
expect(options.map((o) => o.groupLabel)).toEqual([
|
|
|
|
'Vertical axis',
|
|
|
|
'Horizontal axis',
|
|
|
|
'Break down by',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
it('should only accept bucketed operations for x', () => {
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: exampleState(),
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
2020-05-22 09:08:58 +02:00
|
|
|
const filterOperations = options.find((o) => o.groupId === 'x')!.filterOperations;
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
const exampleOperation: Operation = {
|
|
|
|
dataType: 'number',
|
|
|
|
isBucketed: false,
|
|
|
|
label: 'bar',
|
|
|
|
};
|
|
|
|
const bucketedOps: Operation[] = [
|
|
|
|
{ ...exampleOperation, isBucketed: true, dataType: 'number' },
|
|
|
|
{ ...exampleOperation, isBucketed: true, dataType: 'string' },
|
|
|
|
{ ...exampleOperation, isBucketed: true, dataType: 'boolean' },
|
|
|
|
{ ...exampleOperation, isBucketed: true, dataType: 'date' },
|
|
|
|
];
|
|
|
|
const ops: Operation[] = [
|
|
|
|
...bucketedOps,
|
|
|
|
{ ...exampleOperation, dataType: 'number' },
|
|
|
|
{ ...exampleOperation, dataType: 'string' },
|
|
|
|
{ ...exampleOperation, dataType: 'boolean' },
|
|
|
|
{ ...exampleOperation, dataType: 'date' },
|
|
|
|
];
|
|
|
|
expect(ops.filter(filterOperations)).toEqual(bucketedOps);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not allow anything to be added to x', () => {
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: exampleState(),
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
2020-05-22 09:08:58 +02:00
|
|
|
expect(options.find((o) => o.groupId === 'x')?.supportsMoreColumns).toBe(false);
|
2020-03-17 14:57:52 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow number operations on y', () => {
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: exampleState(),
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
2020-05-22 09:08:58 +02:00
|
|
|
const filterOperations = options.find((o) => o.groupId === 'y')!.filterOperations;
|
2020-03-17 14:57:52 +01:00
|
|
|
const exampleOperation: Operation = {
|
|
|
|
dataType: 'number',
|
|
|
|
isBucketed: false,
|
|
|
|
label: 'bar',
|
|
|
|
};
|
|
|
|
const ops: Operation[] = [
|
|
|
|
{ ...exampleOperation, dataType: 'number' },
|
|
|
|
{ ...exampleOperation, dataType: 'string' },
|
|
|
|
{ ...exampleOperation, dataType: 'boolean' },
|
|
|
|
{ ...exampleOperation, dataType: 'date' },
|
|
|
|
];
|
2020-05-22 09:08:58 +02:00
|
|
|
expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']);
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|
2020-11-17 10:19:13 +01:00
|
|
|
|
|
|
|
describe('color assignment', () => {
|
2021-02-03 12:03:55 +01:00
|
|
|
function callConfig(layerConfigOverride: Partial<XYLayerConfig>) {
|
2020-11-17 10:19:13 +01:00
|
|
|
const baseState = exampleState();
|
|
|
|
const options = xyVisualization.getConfiguration({
|
|
|
|
state: {
|
|
|
|
...baseState,
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
...baseState.layers[0],
|
|
|
|
splitAccessor: undefined,
|
|
|
|
...layerConfigOverride,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
frame,
|
|
|
|
layerId: 'first',
|
|
|
|
}).groups;
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
2021-02-03 12:03:55 +01:00
|
|
|
function callConfigForYConfigs(layerConfigOverride: Partial<XYLayerConfig>) {
|
2020-11-17 10:19:13 +01:00
|
|
|
return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'y');
|
|
|
|
}
|
|
|
|
|
2021-02-03 12:03:55 +01:00
|
|
|
function callConfigForBreakdownConfigs(layerConfigOverride: Partial<XYLayerConfig>) {
|
2020-11-17 10:19:13 +01:00
|
|
|
return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'breakdown');
|
|
|
|
}
|
|
|
|
|
|
|
|
function callConfigAndFindYConfig(
|
2021-02-03 12:03:55 +01:00
|
|
|
layerConfigOverride: Partial<XYLayerConfig>,
|
2020-11-17 10:19:13 +01:00
|
|
|
assertionAccessor: string
|
|
|
|
) {
|
|
|
|
const accessorConfig = callConfigForYConfigs(layerConfigOverride)?.accessors.find(
|
|
|
|
(accessor) => typeof accessor !== 'string' && accessor.columnId === assertionAccessor
|
|
|
|
);
|
|
|
|
if (!accessorConfig || typeof accessorConfig === 'string') {
|
|
|
|
throw new Error('could not find accessor');
|
|
|
|
}
|
|
|
|
return accessorConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
it('should pass custom y color in accessor config', () => {
|
|
|
|
const accessorConfig = callConfigAndFindYConfig(
|
|
|
|
{
|
|
|
|
yConfig: [
|
|
|
|
{
|
|
|
|
forAccessor: 'b',
|
|
|
|
color: 'red',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
'b'
|
|
|
|
);
|
|
|
|
expect(accessorConfig.triggerIcon).toEqual('color');
|
|
|
|
expect(accessorConfig.color).toEqual('red');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should query palette to fill in colors for other dimensions', () => {
|
|
|
|
const palette = paletteServiceMock.get('default');
|
|
|
|
(palette.getColor as jest.Mock).mockClear();
|
|
|
|
const accessorConfig = callConfigAndFindYConfig({}, 'c');
|
|
|
|
expect(accessorConfig.triggerIcon).toEqual('color');
|
|
|
|
// black is the color returned from the palette mock
|
|
|
|
expect(accessorConfig.color).toEqual('black');
|
|
|
|
expect(palette.getColor).toHaveBeenCalledWith(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: 'c',
|
|
|
|
// rank 1 because it's the second y metric
|
|
|
|
rankAtDepth: 1,
|
|
|
|
totalSeriesAtDepth: 2,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
{ maxDepth: 1, totalSeries: 2 },
|
|
|
|
undefined
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should pass name of current series along', () => {
|
|
|
|
(frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({
|
|
|
|
label: 'Overwritten label',
|
|
|
|
});
|
|
|
|
const palette = paletteServiceMock.get('default');
|
|
|
|
(palette.getColor as jest.Mock).mockClear();
|
|
|
|
callConfigAndFindYConfig({}, 'c');
|
|
|
|
expect(palette.getColor).toHaveBeenCalledWith(
|
|
|
|
[
|
|
|
|
expect.objectContaining({
|
|
|
|
name: 'Overwritten label',
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
expect.anything(),
|
|
|
|
undefined
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should use custom palette if layer contains palette', () => {
|
|
|
|
const palette = paletteServiceMock.get('mock');
|
|
|
|
callConfigAndFindYConfig(
|
|
|
|
{
|
|
|
|
palette: { type: 'palette', name: 'mock', params: {} },
|
|
|
|
},
|
|
|
|
'c'
|
|
|
|
);
|
|
|
|
expect(palette.getColor).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not show any indicator as long as there is no data', () => {
|
|
|
|
frame.activeData = undefined;
|
|
|
|
const yConfigs = callConfigForYConfigs({});
|
|
|
|
expect(yConfigs!.accessors.length).toEqual(2);
|
|
|
|
yConfigs!.accessors.forEach((accessor) => {
|
|
|
|
expect(accessor.triggerIcon).toBeUndefined();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should show disable icon for splitted series', () => {
|
|
|
|
const accessorConfig = callConfigAndFindYConfig(
|
|
|
|
{
|
|
|
|
splitAccessor: 'd',
|
|
|
|
},
|
|
|
|
'b'
|
|
|
|
);
|
|
|
|
expect(accessorConfig.triggerIcon).toEqual('disabled');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should show current palette for break down by dimension', () => {
|
|
|
|
const palette = paletteServiceMock.get('mock');
|
|
|
|
const customColors = ['yellow', 'green'];
|
|
|
|
(palette.getColors as jest.Mock).mockReturnValue(customColors);
|
|
|
|
const breakdownConfig = callConfigForBreakdownConfigs({
|
|
|
|
palette: { type: 'palette', name: 'mock', params: {} },
|
|
|
|
splitAccessor: 'd',
|
|
|
|
});
|
|
|
|
const accessorConfig = breakdownConfig!.accessors[0];
|
|
|
|
expect(typeof accessorConfig !== 'string' && accessorConfig.palette).toEqual(customColors);
|
|
|
|
});
|
2020-12-03 20:30:42 +01:00
|
|
|
|
|
|
|
it('should respect the order of accessors coming from datasource', () => {
|
|
|
|
mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([
|
|
|
|
{ columnId: 'c' },
|
|
|
|
{ columnId: 'b' },
|
|
|
|
]);
|
|
|
|
const paletteGetter = jest.spyOn(paletteServiceMock, 'get');
|
|
|
|
// overrite palette with a palette returning first blue, then green as color
|
|
|
|
paletteGetter.mockReturnValue({
|
|
|
|
id: 'default',
|
|
|
|
title: '',
|
|
|
|
getColors: jest.fn(),
|
|
|
|
toExpression: jest.fn(),
|
|
|
|
getColor: jest.fn().mockReturnValueOnce('blue').mockReturnValueOnce('green'),
|
|
|
|
});
|
|
|
|
|
|
|
|
const yConfigs = callConfigForYConfigs({});
|
|
|
|
expect(yConfigs?.accessors[0].columnId).toEqual('c');
|
|
|
|
expect(yConfigs?.accessors[0].color).toEqual('blue');
|
|
|
|
expect(yConfigs?.accessors[1].columnId).toEqual('b');
|
|
|
|
expect(yConfigs?.accessors[1].color).toEqual('green');
|
|
|
|
|
|
|
|
paletteGetter.mockClear();
|
|
|
|
});
|
2020-11-17 10:19:13 +01:00
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|
2020-11-04 18:28:00 +01:00
|
|
|
|
|
|
|
describe('#getErrorMessages', () => {
|
2021-03-09 15:31:17 +01:00
|
|
|
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
|
|
|
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
frame = createMockFramePublicAPI();
|
|
|
|
mockDatasource = createMockDatasource('testDatasource');
|
|
|
|
|
|
|
|
mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
|
|
|
dataType: 'string',
|
|
|
|
label: 'MyOperation',
|
|
|
|
} as Operation);
|
|
|
|
|
|
|
|
frame.datasourceLayers = {
|
|
|
|
first: mockDatasource.publicAPIMock,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2020-11-04 18:28:00 +01:00
|
|
|
it("should not return an error when there's only one dimension (X or Y)", () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).not.toBeDefined();
|
|
|
|
});
|
|
|
|
it("should not return an error when there's only one dimension on multiple layers (same axis everywhere)", () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).not.toBeDefined();
|
|
|
|
});
|
|
|
|
it('should not return an error when mixing different valid configurations in multiple layers', () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['a'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: ['a'],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).not.toBeDefined();
|
|
|
|
});
|
|
|
|
it("should not return an error when there's only one splitAccessor dimension configured", () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).not.toBeDefined();
|
|
|
|
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).not.toBeDefined();
|
|
|
|
});
|
|
|
|
it('should return an error when there are multiple layers, one axis configured for each layer (but different axis from each other)', () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: ['a'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).toEqual([
|
|
|
|
{
|
|
|
|
shortMessage: 'Missing Vertical axis.',
|
|
|
|
longMessage: 'Layer 1 requires a field for the Vertical axis.',
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
it('should return an error with batched messages for the same error with multiple layers', () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['a'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'third',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: undefined,
|
|
|
|
accessors: [],
|
|
|
|
splitAccessor: 'a',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).toEqual([
|
|
|
|
{
|
|
|
|
shortMessage: 'Missing Vertical axis.',
|
|
|
|
longMessage: 'Layers 2, 3 require a field for the Vertical axis.',
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
it("should return an error when some layers are complete but other layers aren't", () => {
|
|
|
|
expect(
|
2021-02-15 13:00:31 +01:00
|
|
|
xyVisualization.getErrorMessages({
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: [],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'second',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['a'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
layerId: 'third',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['a'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2020-11-04 18:28:00 +01:00
|
|
|
).toEqual([
|
|
|
|
{
|
|
|
|
shortMessage: 'Missing Vertical axis.',
|
|
|
|
longMessage: 'Layer 1 requires a field for the Vertical axis.',
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
2021-03-09 15:31:17 +01:00
|
|
|
|
|
|
|
it('should return an error when accessor type is of the wrong type', () => {
|
|
|
|
expect(
|
|
|
|
xyVisualization.getErrorMessages(
|
|
|
|
{
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
splitAccessor: 'd',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['b'], // just use a single accessor to avoid too much noise
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
frame.datasourceLayers
|
|
|
|
)
|
|
|
|
).toEqual([
|
|
|
|
{
|
|
|
|
shortMessage: 'Wrong data type for Vertical axis.',
|
|
|
|
longMessage:
|
|
|
|
'The dimension MyOperation provided for the Vertical axis has the wrong data type. Expected number but have string',
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
2020-11-04 18:28:00 +01:00
|
|
|
});
|
2020-12-02 19:55:58 +01:00
|
|
|
|
|
|
|
describe('#getWarningMessages', () => {
|
|
|
|
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
|
|
|
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
frame = createMockFramePublicAPI();
|
|
|
|
mockDatasource = createMockDatasource('testDatasource');
|
|
|
|
|
|
|
|
mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([
|
|
|
|
{ columnId: 'd' },
|
|
|
|
{ columnId: 'a' },
|
|
|
|
{ columnId: 'b' },
|
|
|
|
{ columnId: 'c' },
|
|
|
|
]);
|
|
|
|
|
|
|
|
frame.datasourceLayers = {
|
|
|
|
first: mockDatasource.publicAPIMock,
|
|
|
|
};
|
|
|
|
|
|
|
|
frame.activeData = {
|
|
|
|
first: {
|
|
|
|
type: 'datatable',
|
|
|
|
columns: [
|
|
|
|
{ id: 'a', name: 'A', meta: { type: 'number' } },
|
|
|
|
{ id: 'b', name: 'B', meta: { type: 'number' } },
|
|
|
|
],
|
|
|
|
rows: [
|
|
|
|
{ a: 1, b: [2, 0] },
|
|
|
|
{ a: 3, b: 4 },
|
|
|
|
{ a: 5, b: 6 },
|
|
|
|
{ a: 7, b: 8 },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
it('should return a warning when numeric accessors contain array', () => {
|
|
|
|
(frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({
|
|
|
|
label: 'Label B',
|
|
|
|
});
|
|
|
|
const warningMessages = xyVisualization.getWarningMessages!(
|
|
|
|
{
|
|
|
|
...exampleState(),
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: 'first',
|
|
|
|
seriesType: 'area',
|
|
|
|
xAccessor: 'a',
|
|
|
|
accessors: ['b'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
frame
|
|
|
|
);
|
|
|
|
expect(warningMessages).toHaveLength(1);
|
|
|
|
expect(warningMessages && warningMessages[0]).toMatchInlineSnapshot(`
|
|
|
|
<React.Fragment>
|
|
|
|
<strong>
|
|
|
|
Label B
|
|
|
|
</strong>
|
|
|
|
contains array values. Your visualization may not render as expected.
|
|
|
|
</React.Fragment>
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
});
|
2019-09-17 20:57:53 +02:00
|
|
|
});
|