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
|
|
|
*/
|
|
|
|
|
|
|
|
import React from 'react';
|
|
|
|
import _ from 'lodash';
|
|
|
|
import { render } from 'react-dom';
|
|
|
|
import { Position } from '@elastic/charts';
|
|
|
|
import { I18nProvider } from '@kbn/i18n/react';
|
|
|
|
import { i18n } from '@kbn/i18n';
|
2020-12-02 10:06:25 +01:00
|
|
|
import { PaletteRegistry } from 'src/plugins/charts/public';
|
2020-11-17 10:19:13 +01:00
|
|
|
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
2019-09-17 20:57:53 +02:00
|
|
|
import { getSuggestions } from './xy_suggestions';
|
2020-07-03 16:15:06 +02:00
|
|
|
import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
|
2020-12-02 10:06:25 +01:00
|
|
|
import { Visualization, OperationMetadata, VisualizationType, AccessorConfig } from '../types';
|
2021-02-03 12:03:55 +01:00
|
|
|
import { State, SeriesType, visualizationTypes, XYLayerConfig } from './types';
|
2020-12-02 10:06:25 +01:00
|
|
|
import { isHorizontalChart } from './state_helpers';
|
2020-11-03 21:18:44 +01:00
|
|
|
import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression';
|
2020-09-15 11:24:02 +02:00
|
|
|
import { LensIconChartBarStacked } from '../assets/chart_bar_stacked';
|
|
|
|
import { LensIconChartMixedXy } from '../assets/chart_mixed_xy';
|
|
|
|
import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal';
|
2020-12-02 10:06:25 +01:00
|
|
|
import { getAccessorColorConfig, getColorAssignments } from './color_assignment';
|
2020-12-02 19:55:58 +01:00
|
|
|
import { getColumnToLabelMap } from './state_helpers';
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-09-15 11:24:02 +02:00
|
|
|
const defaultIcon = LensIconChartBarStacked;
|
2019-10-01 21:04:00 +02:00
|
|
|
const defaultSeriesType = 'bar_stacked';
|
2020-03-17 14:57:52 +01:00
|
|
|
const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number';
|
|
|
|
const isBucketed = (op: OperationMetadata) => op.isBucketed;
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-04-30 01:14:47 +02:00
|
|
|
function getVisualizationType(state: State): VisualizationType | 'mixed' {
|
|
|
|
if (!state.layers.length) {
|
|
|
|
return (
|
2020-05-22 09:08:58 +02:00
|
|
|
visualizationTypes.find((t) => t.id === state.preferredSeriesType) ?? visualizationTypes[0]
|
2020-04-30 01:14:47 +02:00
|
|
|
);
|
|
|
|
}
|
2020-05-22 09:08:58 +02:00
|
|
|
const visualizationType = visualizationTypes.find((t) => t.id === state.layers[0].seriesType);
|
chore(NA): upgrade to lodash@4 (#69868)
* chore(NA): upgrade oss to lodash4
chore(NA): migrate cli, cli_plugin, cli_keystore, dev, test_utils and apm src script to lodash4
chore(NA): missing file for cli plugin
chore(NA): add src core
chore(NA): es archiver and fixtures
chore(NA): try to fix functional test failure
chore(NA): migrate src/legacy entirely to lodash4 except src/legacy/core_plugins
chore(NA): move legacy core plugins to lodash4
chore(NA): upgrade optimize to lodash4
chore(NA): upgrade to lodash4 on advanced_settings, charts, console and dashboard
chore(NA): migrate to lodash4 on dev_tools, discover, embeddable, es_ui)shared, expressions, home plugins
chore(NA): upgrade data plugin to lodash4
chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4
chore(NA): missing data upgrades to lodash4
Revert "chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4"
This reverts commit 137055c5fed2fc52bb26547e0bc1ad2e3d4fe309.
Revert "Revert "chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4""
This reverts commit f7e73688782998513d9fb6d7e8f0765e9beb28d1.
Revert "chore(NA): missing data upgrades to lodash4"
This reverts commit 92b85bf947a89bfc70cc4052738a6b2128ffb076.
Revert "chore(NA): upgrade data plugin to lodash4"
This reverts commit 88fdb075ee1e26c4ac979b6681d8a2b002df74c6.
chore(NA): upgrade idx_pattern_mgt, input_control_vis, inspector, kbn_legacy, kbn_react, kbn_usage_collections, kbn_utils, management and maps_legacy to lodash4
chore(NA): map src plugin data to lodash3
chore(NA): missing lodash.clonedeep dep
chore(NA): change packages kbn-config-schema deps
chore(NA): update renovate config
chore(NA): upgrade vis_type plugins to lodash4
chore(NA): move vis_type_vislib to lodash3
chore(NA): update visualizations and visualize to lodash4
chore(NA): remove lodash 3 types from src and move test to lodash4
chore(NA): move home, usage_collection and management to lodash 3
Revert "chore(NA): move home, usage_collection and management to lodash 3"
This reverts commit f86e8585f02d21550746569af54215b076a79a3d.
chore(NA): move kibana_legacy, saved_objects saved_objects_management into lodash3
chore(NA): update x-pack test to mock lodash4
Revert "chore(NA): move kibana_legacy, saved_objects saved_objects_management into lodash3"
This reverts commit 2d10fe450533e1b36db21d99cfae3ce996a244e0.
* chore(NA): move x-pack and packages to lodash 4
* chore(NA): remove mention to lodash from main package.json
* chore(NA): remove helper alias for lodash4 and make it the default lodash
* chore(NA): fix last failing types in the repo
* chore(NA): fix public api
* chore(NA): fix types for agg_row.tsx
* chore(NA): fix increment of optimizer modules in the rollup plugin
* chore(NA): migrate `src/core/public/http/fetch.ts` (#5)
* omit undefined query props
* just remove merge usage
* fix types
* chore(NA): fixes for feedback from apm team
* chore(NA): recover old behaviour on apm LoadingIndeicatorContext.tsx
* chore(NA): fixes for feedback from watson
* Platform lodash4 tweaks (#6)
* chore(NA): fix types and behaviour on src/core/server/elasticsearch/errors.ts
* Canvas fixes for lodash upgrade
* [APM] Adds unit test for APM service maps transform (#7)
* Adds a snapshot unit test for getConnections and rearranges some code to make testing easier
* reverts `ArrayList` back to `String[]` in the painless script within `fetch_service_paths_from_trace_ids.ts`
* chore(NA): update yarn.lock
* chore(NA): remove any and use a real type for alerts task runner
Co-authored-by: Gidi Meir Morris <github@gidi.io>
* chore(NA): used named import for triggers_actions_ui file
* chore(NA): fix eslint
* chore(NA): fix types
* Delete most uptime lodash references.
* Simplify. Clean up types.
* [Uptime] Delete most uptime lodash references (#8)
* Delete most uptime lodash references.
* Simplify. Clean up types.
* chore(NA): add eslint rule to avoid using lodash3
* chore(NA): apply changes on feedback from es-ui team
* fix some types (#9)
* Clean up some expressions types.
* chore(NA): missing ts-expect-error statements
* Upgrade lodash 4 vislib (#11)
* replace lodash 3 with lodash 4 on vislib plugin
* Further changes
* further replacement of lodash3 to 4
* further work on upgrading to lodash 4
* final changes to update lodash
* chore(NA): upgrade data plugin to lodash4
chore(NA): upgrade data plugin public to lodash4
chore(NA): fix typecheck task
chore(NA): fix agg_config with hasIn
chore(NA): assign to assignIn and has to hasIn
chore(NA): upgrade data plugin server to lodash4
chore(NA): new signature for core api
fix(NA): match behaviour between lodash3 and lodash4 for set in search_source
* chore(NA): remove lodash3 completely from the repo
* chore(NA): fix x-pack/test/api_integration/apis/metrics_ui/snapshot.ts missing content
* chore(NA): fix lodash usage on apm
* chore(NA): fix typecheck for maps
* Patch lodash template (#12)
* Applying changes from https://github.com/elastic/kibana/pull/64985
* Using isIterateeCall, because it seems less brittle
* Also patching `lodash/template` and `lodash/fp/template`
* Reorganizing some files...
* Revising comment
* Ends up `_` is a function also... I hate JavaScript
Co-authored-by: Pierre Gayvallet <pierre.gayvallet@gmail.com>
Co-authored-by: Josh Dover <me@joshdover.com>
Co-authored-by: Clint Andrew Hall <clint.hall@elastic.co>
Co-authored-by: Oliver Gupte <ogupte@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Gidi Meir Morris <github@gidi.io>
Co-authored-by: Justin Kambic <justin.kambic@elastic.co>
Co-authored-by: Stratoula Kalafateli <stratoula1@gmail.com>
Co-authored-by: Luke Elmers <luke.elmers@elastic.co>
Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com>
Co-authored-by: kobelb <brandon.kobel@elastic.co>
2020-07-03 02:30:13 +02:00
|
|
|
const seriesTypes = _.uniq(state.layers.map((l) => l.seriesType));
|
2020-04-30 01:14:47 +02:00
|
|
|
|
|
|
|
return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed';
|
|
|
|
}
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
function getDescription(state?: State) {
|
|
|
|
if (!state) {
|
|
|
|
return {
|
|
|
|
icon: defaultIcon,
|
|
|
|
label: i18n.translate('xpack.lens.xyVisualization.xyLabel', {
|
2019-10-02 19:21:39 +02:00
|
|
|
defaultMessage: 'XY',
|
2019-09-17 20:57:53 +02:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-30 01:14:47 +02:00
|
|
|
const visualizationType = getVisualizationType(state);
|
|
|
|
|
2020-09-15 11:24:02 +02:00
|
|
|
if (visualizationType === 'mixed' && isHorizontalChart(state.layers)) {
|
|
|
|
return {
|
|
|
|
icon: LensIconChartBarHorizontal,
|
|
|
|
label: i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
|
2021-03-02 11:25:35 +01:00
|
|
|
defaultMessage: 'Mixed bar horizontal',
|
2020-09-15 11:24:02 +02:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (visualizationType === 'mixed') {
|
2019-10-23 22:28:18 +02:00
|
|
|
return {
|
2020-09-15 11:24:02 +02:00
|
|
|
icon: LensIconChartMixedXy,
|
|
|
|
label: i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
|
|
|
|
defaultMessage: 'Mixed XY',
|
|
|
|
}),
|
2019-10-23 22:28:18 +02:00
|
|
|
};
|
2019-09-17 20:57:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2020-09-15 11:24:02 +02:00
|
|
|
icon: visualizationType.icon,
|
2021-03-02 11:25:35 +01:00
|
|
|
label: visualizationType.fullLabel || visualizationType.label,
|
2019-09-17 20:57:53 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-11-04 11:27:52 +01:00
|
|
|
export const getXyVisualization = ({
|
|
|
|
paletteService,
|
2020-11-17 10:19:13 +01:00
|
|
|
data,
|
2020-11-04 11:27:52 +01:00
|
|
|
}: {
|
|
|
|
paletteService: PaletteRegistry;
|
2020-11-17 10:19:13 +01:00
|
|
|
data: DataPublicPluginStart;
|
2020-11-04 11:27:52 +01:00
|
|
|
}): Visualization<State> => ({
|
2019-09-17 20:57:53 +02:00
|
|
|
id: 'lnsXY',
|
|
|
|
|
|
|
|
visualizationTypes,
|
2020-04-30 01:14:47 +02:00
|
|
|
getVisualizationTypeId(state) {
|
|
|
|
const type = getVisualizationType(state);
|
|
|
|
return type === 'mixed' ? type : type.id;
|
|
|
|
},
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-01-14 18:06:51 +01:00
|
|
|
getLayerIds(state) {
|
2020-05-22 09:08:58 +02:00
|
|
|
return state.layers.map((l) => l.layerId);
|
2020-01-14 18:06:51 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
removeLayer(state, layerId) {
|
|
|
|
return {
|
|
|
|
...state,
|
2020-05-22 09:08:58 +02:00
|
|
|
layers: state.layers.filter((l) => l.layerId !== layerId),
|
2020-01-14 18:06:51 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
appendLayer(state, layerId) {
|
2020-05-22 09:08:58 +02:00
|
|
|
const usedSeriesTypes = _.uniq(state.layers.map((layer) => layer.seriesType));
|
2020-01-14 18:06:51 +01:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
layers: [
|
|
|
|
...state.layers,
|
|
|
|
newLayerState(
|
|
|
|
usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
|
|
|
|
layerId
|
|
|
|
),
|
|
|
|
],
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
clearLayer(state, layerId) {
|
|
|
|
return {
|
|
|
|
...state,
|
2020-05-22 09:08:58 +02:00
|
|
|
layers: state.layers.map((l) =>
|
2020-01-14 18:06:51 +01:00
|
|
|
l.layerId !== layerId ? l : newLayerState(state.preferredSeriesType, layerId)
|
|
|
|
),
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
getDescription(state) {
|
|
|
|
const { icon, label } = getDescription(state);
|
2019-10-02 19:21:39 +02:00
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
return {
|
|
|
|
icon: icon || defaultIcon,
|
2020-10-06 17:17:41 +02:00
|
|
|
label,
|
2019-09-17 20:57:53 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
switchVisualizationType(seriesType: string, state: State) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
preferredSeriesType: seriesType as SeriesType,
|
2020-05-22 09:08:58 +02:00
|
|
|
layers: state.layers.map((layer) => ({ ...layer, seriesType: seriesType as SeriesType })),
|
2019-09-17 20:57:53 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
getSuggestions,
|
|
|
|
|
|
|
|
initialize(frame, state) {
|
|
|
|
return (
|
|
|
|
state || {
|
2019-10-25 18:09:41 +02:00
|
|
|
title: 'Empty XY chart',
|
2019-09-17 20:57:53 +02:00
|
|
|
legend: { isVisible: true, position: Position.Right },
|
2020-11-06 16:34:30 +01:00
|
|
|
valueLabels: 'hide',
|
2019-09-17 20:57:53 +02:00
|
|
|
preferredSeriesType: defaultSeriesType,
|
|
|
|
layers: [
|
|
|
|
{
|
|
|
|
layerId: frame.addNewLayer(),
|
2020-03-17 14:57:52 +01:00
|
|
|
accessors: [],
|
2019-09-17 20:57:53 +02:00
|
|
|
position: Position.Top,
|
|
|
|
seriesType: defaultSeriesType,
|
|
|
|
showGridlines: false,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2020-11-03 21:18:44 +01:00
|
|
|
getConfiguration({ state, frame, layerId }) {
|
|
|
|
const layer = state.layers.find((l) => l.layerId === layerId);
|
|
|
|
if (!layer) {
|
|
|
|
return { groups: [] };
|
|
|
|
}
|
|
|
|
|
|
|
|
const datasource = frame.datasourceLayers[layer.layerId];
|
|
|
|
|
2020-11-17 10:19:13 +01:00
|
|
|
const sortedAccessors: string[] = getSortedAccessors(datasource, layer);
|
|
|
|
let mappedAccessors: AccessorConfig[] = sortedAccessors.map((accessor) => ({
|
|
|
|
columnId: accessor,
|
|
|
|
}));
|
|
|
|
|
|
|
|
if (frame.activeData) {
|
|
|
|
const colorAssignments = getColorAssignments(
|
|
|
|
state.layers,
|
|
|
|
{ tables: frame.activeData },
|
|
|
|
data.fieldFormats.deserialize
|
|
|
|
);
|
|
|
|
mappedAccessors = getAccessorColorConfig(
|
|
|
|
colorAssignments,
|
|
|
|
frame,
|
2020-12-03 20:30:42 +01:00
|
|
|
{
|
|
|
|
...layer,
|
|
|
|
accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)),
|
|
|
|
},
|
2020-11-17 10:19:13 +01:00
|
|
|
paletteService
|
|
|
|
);
|
|
|
|
}
|
2020-11-03 21:18:44 +01:00
|
|
|
|
|
|
|
const isHorizontal = isHorizontalChart(state.layers);
|
2020-03-17 14:57:52 +01:00
|
|
|
return {
|
|
|
|
groups: [
|
|
|
|
{
|
|
|
|
groupId: 'x',
|
2020-11-04 18:28:00 +01:00
|
|
|
groupLabel: getAxisName('x', { isHorizontal }),
|
2020-11-17 10:19:13 +01:00
|
|
|
accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [],
|
2020-03-17 14:57:52 +01:00
|
|
|
filterOperations: isBucketed,
|
|
|
|
supportsMoreColumns: !layer.xAccessor,
|
|
|
|
dataTestSubj: 'lnsXY_xDimensionPanel',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
groupId: 'y',
|
2020-11-04 18:28:00 +01:00
|
|
|
groupLabel: getAxisName('y', { isHorizontal }),
|
2020-11-17 10:19:13 +01:00
|
|
|
accessors: mappedAccessors,
|
2020-03-17 14:57:52 +01:00
|
|
|
filterOperations: isNumericMetric,
|
|
|
|
supportsMoreColumns: true,
|
|
|
|
required: true,
|
|
|
|
dataTestSubj: 'lnsXY_yDimensionPanel',
|
2020-07-01 09:57:23 +02:00
|
|
|
enableDimensionEditor: true,
|
2020-03-17 14:57:52 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
groupId: 'breakdown',
|
|
|
|
groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', {
|
|
|
|
defaultMessage: 'Break down by',
|
|
|
|
}),
|
2020-11-17 10:19:13 +01:00
|
|
|
accessors: layer.splitAccessor
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
columnId: layer.splitAccessor,
|
|
|
|
triggerIcon: 'colorBy',
|
|
|
|
palette: paletteService
|
|
|
|
.get(layer.palette?.name || 'default')
|
|
|
|
.getColors(10, layer.palette?.params),
|
|
|
|
},
|
|
|
|
]
|
|
|
|
: [],
|
2020-03-17 14:57:52 +01:00
|
|
|
filterOperations: isBucketed,
|
|
|
|
supportsMoreColumns: !layer.splitAccessor,
|
|
|
|
dataTestSubj: 'lnsXY_splitDimensionPanel',
|
2020-09-15 11:24:02 +02:00
|
|
|
required: layer.seriesType.includes('percentage'),
|
2020-11-04 11:27:52 +01:00
|
|
|
enableDimensionEditor: true,
|
2020-03-17 14:57:52 +01:00
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2020-11-04 11:27:52 +01:00
|
|
|
getMainPalette: (state) => {
|
|
|
|
if (!state || state.layers.length === 0) return;
|
|
|
|
return state.layers[0].palette;
|
|
|
|
},
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
setDimension({ prevState, layerId, columnId, groupId }) {
|
2020-05-22 09:08:58 +02:00
|
|
|
const newLayer = prevState.layers.find((l) => l.layerId === layerId);
|
2020-03-17 14:57:52 +01:00
|
|
|
if (!newLayer) {
|
|
|
|
return prevState;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (groupId === 'x') {
|
|
|
|
newLayer.xAccessor = columnId;
|
|
|
|
}
|
|
|
|
if (groupId === 'y') {
|
2020-05-22 09:08:58 +02:00
|
|
|
newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId];
|
2020-03-17 14:57:52 +01:00
|
|
|
}
|
|
|
|
if (groupId === 'breakdown') {
|
|
|
|
newLayer.splitAccessor = columnId;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...prevState,
|
2020-05-22 09:08:58 +02:00
|
|
|
layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)),
|
2020-03-17 14:57:52 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
removeDimension({ prevState, layerId, columnId }) {
|
2020-05-22 09:08:58 +02:00
|
|
|
const newLayer = prevState.layers.find((l) => l.layerId === layerId);
|
2020-03-17 14:57:52 +01:00
|
|
|
if (!newLayer) {
|
|
|
|
return prevState;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newLayer.xAccessor === columnId) {
|
|
|
|
delete newLayer.xAccessor;
|
|
|
|
} else if (newLayer.splitAccessor === columnId) {
|
|
|
|
delete newLayer.splitAccessor;
|
2020-11-04 11:27:52 +01:00
|
|
|
// as the palette is associated with the break down by dimension, remove it together with the dimension
|
|
|
|
delete newLayer.palette;
|
2020-03-17 14:57:52 +01:00
|
|
|
} else if (newLayer.accessors.includes(columnId)) {
|
2020-05-22 09:08:58 +02:00
|
|
|
newLayer.accessors = newLayer.accessors.filter((a) => a !== columnId);
|
2020-03-17 14:57:52 +01:00
|
|
|
}
|
|
|
|
|
2020-07-01 09:57:23 +02:00
|
|
|
if (newLayer.yConfig) {
|
|
|
|
newLayer.yConfig = newLayer.yConfig.filter(({ forAccessor }) => forAccessor !== columnId);
|
|
|
|
}
|
|
|
|
|
2020-03-17 14:57:52 +01:00
|
|
|
return {
|
|
|
|
...prevState,
|
2020-05-22 09:08:58 +02:00
|
|
|
layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)),
|
2020-03-17 14:57:52 +01:00
|
|
|
};
|
|
|
|
},
|
2019-09-17 20:57:53 +02:00
|
|
|
|
2020-01-14 18:06:51 +01:00
|
|
|
getLayerContextMenuIcon({ state, layerId }) {
|
2020-05-22 09:08:58 +02:00
|
|
|
const layer = state.layers.find((l) => l.layerId === layerId);
|
2020-11-30 15:45:26 +01:00
|
|
|
const visualizationType = visualizationTypes.find((t) => t.id === layer?.seriesType);
|
|
|
|
return {
|
|
|
|
icon: visualizationType?.icon || 'gear',
|
|
|
|
label: visualizationType?.label || '',
|
|
|
|
};
|
2020-01-14 18:06:51 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
renderLayerContextMenu(domElement, props) {
|
|
|
|
render(
|
|
|
|
<I18nProvider>
|
|
|
|
<LayerContextMenu {...props} />
|
|
|
|
</I18nProvider>,
|
|
|
|
domElement
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2020-07-03 16:15:06 +02:00
|
|
|
renderToolbar(domElement, props) {
|
|
|
|
render(
|
|
|
|
<I18nProvider>
|
|
|
|
<XyToolbar {...props} />
|
|
|
|
</I18nProvider>,
|
|
|
|
domElement
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2020-07-01 09:57:23 +02:00
|
|
|
renderDimensionEditor(domElement, props) {
|
|
|
|
render(
|
|
|
|
<I18nProvider>
|
2020-12-02 10:06:25 +01:00
|
|
|
<DimensionEditor
|
|
|
|
{...props}
|
|
|
|
formatFactory={data.fieldFormats.deserialize}
|
|
|
|
paletteService={paletteService}
|
|
|
|
/>
|
2020-07-01 09:57:23 +02:00
|
|
|
</I18nProvider>,
|
|
|
|
domElement
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2020-11-04 11:27:52 +01:00
|
|
|
toExpression: (state, layers, attributes) =>
|
|
|
|
toExpression(state, layers, paletteService, attributes),
|
|
|
|
toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService),
|
2020-11-04 18:28:00 +01:00
|
|
|
|
2021-03-09 15:31:17 +01:00
|
|
|
getErrorMessages(state, datasourceLayers) {
|
2020-11-04 18:28:00 +01:00
|
|
|
// Data error handling below here
|
2021-02-03 12:03:55 +01:00
|
|
|
const hasNoAccessors = ({ accessors }: XYLayerConfig) =>
|
2020-11-04 18:28:00 +01:00
|
|
|
accessors == null || accessors.length === 0;
|
2021-02-03 12:03:55 +01:00
|
|
|
const hasNoSplitAccessor = ({ splitAccessor, seriesType }: XYLayerConfig) =>
|
2020-11-04 18:28:00 +01:00
|
|
|
seriesType.includes('percentage') && splitAccessor == null;
|
|
|
|
|
|
|
|
const errors: Array<{
|
|
|
|
shortMessage: string;
|
|
|
|
longMessage: string;
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
// check if the layers in the state are compatible with this type of chart
|
|
|
|
if (state && state.layers.length > 1) {
|
|
|
|
// Order is important here: Y Axis is fundamental to exist to make it valid
|
2021-02-03 12:03:55 +01:00
|
|
|
const checks: Array<[string, (layer: XYLayerConfig) => boolean]> = [
|
2020-11-04 18:28:00 +01:00
|
|
|
['Y', hasNoAccessors],
|
|
|
|
['Break down', hasNoSplitAccessor],
|
|
|
|
];
|
|
|
|
|
|
|
|
// filter out those layers with no accessors at all
|
|
|
|
const filteredLayers = state.layers.filter(
|
2021-02-03 12:03:55 +01:00
|
|
|
({ accessors, xAccessor, splitAccessor }: XYLayerConfig) =>
|
2020-11-04 18:28:00 +01:00
|
|
|
accessors.length > 0 || xAccessor != null || splitAccessor != null
|
|
|
|
);
|
|
|
|
for (const [dimension, criteria] of checks) {
|
|
|
|
const result = validateLayersForDimension(dimension, filteredLayers, criteria);
|
|
|
|
if (!result.valid) {
|
|
|
|
errors.push(result.payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 15:31:17 +01:00
|
|
|
if (datasourceLayers && state) {
|
|
|
|
for (const layer of state.layers) {
|
|
|
|
const datasourceAPI = datasourceLayers[layer.layerId];
|
|
|
|
if (datasourceAPI) {
|
|
|
|
for (const accessor of layer.accessors) {
|
|
|
|
const operation = datasourceAPI.getOperationForColumnId(accessor);
|
|
|
|
if (operation && operation.dataType !== 'number') {
|
|
|
|
errors.push({
|
|
|
|
shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYShort', {
|
|
|
|
defaultMessage: `Wrong data type for {axis}.`,
|
|
|
|
values: {
|
|
|
|
axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYLong', {
|
|
|
|
defaultMessage: `The dimension {label} provided for the {axis} has the wrong data type. Expected number but have {dataType}`,
|
|
|
|
values: {
|
|
|
|
label: operation.label,
|
|
|
|
dataType: operation.dataType,
|
|
|
|
axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-04 18:28:00 +01:00
|
|
|
return errors.length ? errors : undefined;
|
|
|
|
},
|
2020-12-02 19:55:58 +01:00
|
|
|
|
|
|
|
getWarningMessages(state, frame) {
|
|
|
|
if (state?.layers.length === 0 || !frame.activeData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const layers = state.layers;
|
|
|
|
|
2021-02-03 12:03:55 +01:00
|
|
|
const filteredLayers = layers.filter(({ accessors }: XYLayerConfig) => accessors.length > 0);
|
2020-12-02 19:55:58 +01:00
|
|
|
const accessorsWithArrayValues = [];
|
|
|
|
for (const layer of filteredLayers) {
|
|
|
|
const { layerId, accessors } = layer;
|
|
|
|
const rows = frame.activeData[layerId] && frame.activeData[layerId].rows;
|
|
|
|
if (!rows) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layerId]);
|
|
|
|
for (const accessor of accessors) {
|
|
|
|
const hasArrayValues = rows.some((row) => Array.isArray(row[accessor]));
|
|
|
|
if (hasArrayValues) {
|
|
|
|
accessorsWithArrayValues.push(columnToLabel[accessor]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return accessorsWithArrayValues.map((label) => (
|
|
|
|
<>
|
|
|
|
<strong>{label}</strong> contains array values. Your visualization may not render as
|
|
|
|
expected.
|
|
|
|
</>
|
|
|
|
));
|
|
|
|
},
|
2020-11-04 11:27:52 +01:00
|
|
|
});
|
2020-01-14 18:06:51 +01:00
|
|
|
|
2020-11-04 18:28:00 +01:00
|
|
|
function validateLayersForDimension(
|
|
|
|
dimension: string,
|
2021-02-03 12:03:55 +01:00
|
|
|
layers: XYLayerConfig[],
|
|
|
|
missingCriteria: (layer: XYLayerConfig) => boolean
|
2020-11-04 18:28:00 +01:00
|
|
|
):
|
|
|
|
| { valid: true }
|
|
|
|
| {
|
|
|
|
valid: false;
|
|
|
|
payload: { shortMessage: string; longMessage: string };
|
|
|
|
} {
|
|
|
|
// Multiple layers must be consistent:
|
|
|
|
// * either a dimension is missing in ALL of them
|
|
|
|
// * or should not miss on any
|
|
|
|
if (layers.every(missingCriteria) || !layers.some(missingCriteria)) {
|
|
|
|
return { valid: true };
|
|
|
|
}
|
|
|
|
// otherwise it's an error and it has to be reported
|
|
|
|
const layerMissingAccessors = layers.reduce((missing: number[], layer, i) => {
|
|
|
|
if (missingCriteria(layer)) {
|
|
|
|
missing.push(i);
|
|
|
|
}
|
|
|
|
return missing;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return {
|
|
|
|
valid: false,
|
|
|
|
payload: getMessageIdsForDimension(dimension, layerMissingAccessors, isHorizontalChart(layers)),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getAxisName(axis: 'x' | 'y', { isHorizontal }: { isHorizontal: boolean }) {
|
|
|
|
const vertical = i18n.translate('xpack.lens.xyChart.verticalAxisLabel', {
|
|
|
|
defaultMessage: 'Vertical axis',
|
|
|
|
});
|
|
|
|
const horizontal = i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', {
|
|
|
|
defaultMessage: 'Horizontal axis',
|
|
|
|
});
|
|
|
|
if (axis === 'x') {
|
|
|
|
return isHorizontal ? vertical : horizontal;
|
|
|
|
}
|
|
|
|
return isHorizontal ? horizontal : vertical;
|
|
|
|
}
|
|
|
|
|
|
|
|
// i18n ids cannot be dynamically generated, hence the function below
|
|
|
|
function getMessageIdsForDimension(dimension: string, layers: number[], isHorizontal: boolean) {
|
|
|
|
const layersList = layers.map((i: number) => i + 1).join(', ');
|
|
|
|
switch (dimension) {
|
|
|
|
case 'Break down':
|
|
|
|
return {
|
|
|
|
shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitShort', {
|
|
|
|
defaultMessage: `Missing {axis}.`,
|
|
|
|
values: { axis: 'Break down by axis' },
|
|
|
|
}),
|
|
|
|
longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitLong', {
|
|
|
|
defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`,
|
|
|
|
values: { layers: layers.length, layersList, axis: 'Break down by axis' },
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
case 'Y':
|
|
|
|
return {
|
|
|
|
shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYShort', {
|
|
|
|
defaultMessage: `Missing {axis}.`,
|
|
|
|
values: { axis: getAxisName('y', { isHorizontal }) },
|
|
|
|
}),
|
|
|
|
longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYLong', {
|
|
|
|
defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`,
|
|
|
|
values: { layers: layers.length, layersList, axis: getAxisName('y', { isHorizontal }) },
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return { shortMessage: '', longMessage: '' };
|
|
|
|
}
|
|
|
|
|
2021-02-03 12:03:55 +01:00
|
|
|
function newLayerState(seriesType: SeriesType, layerId: string): XYLayerConfig {
|
2020-01-14 18:06:51 +01:00
|
|
|
return {
|
|
|
|
layerId,
|
|
|
|
seriesType,
|
2020-03-17 14:57:52 +01:00
|
|
|
accessors: [],
|
2020-01-14 18:06:51 +01:00
|
|
|
};
|
|
|
|
}
|