"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.computeOverallDataDomain = computeOverallDataDomain; exports.getGroupsAvailableInData = getGroupsAvailableInData; exports.getGroupsRelatedToData = getGroupsRelatedToData; exports.getGroupsToShow = getGroupsToShow; exports.getSingleColorConfig = exports.getReferenceSupportedLayer = exports.getReferenceLineAccessorColorConfig = exports.getReferenceConfiguration = void 0; exports.getStaticValue = getStaticValue; exports.setReferenceDimension = void 0; var _lodash = require("lodash"); var _i18n = require("@kbn/i18n"); var _chartIcons = require("@kbn/chart-icons"); var _public = require("@kbn/expression-xy-plugin/public"); var _axes_configuration = require("./axes_configuration"); var _state_helpers = require("./state_helpers"); var _visualization_helpers = require("./visualization_helpers"); var _id_generator = require("../../id_generator"); var _color_assignment = require("./color_assignment"); /* * 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. */ /** * Return the reference layers groups to show based on multiple criteria: * * what groups are current defined in data layers * * what existing reference line are currently defined in reference layers */ function getGroupsToShow(referenceLayers, state, datasourceLayers, tables) { if (!state) { return []; } const dataLayers = (0, _visualization_helpers.getDataLayers)(state.layers); const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); return referenceLayers.filter(({ label, config }) => groupsAvailable[label] || (config === null || config === void 0 ? void 0 : config.length)).map(layer => ({ ...layer, valid: groupsAvailable[layer.label] })); } /** * Returns the reference layers groups to show based on what groups are current defined in data layers. */ function getGroupsRelatedToData(referenceLayers, state, datasourceLayers, tables) { if (!state) { return []; } const dataLayers = (0, _visualization_helpers.getDataLayers)(state.layers); const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); return referenceLayers.filter(({ label }) => groupsAvailable[label]); } /** * Returns a dictionary with the groups filled in all the data layers */ function getGroupsAvailableInData(dataLayers, datasourceLayers, tables) { const hasNumberHistogram = dataLayers.some((0, _visualization_helpers.checkScaleOperation)('interval', 'number', datasourceLayers)); const { right, left } = (0, _axes_configuration.groupAxesByType)(dataLayers, tables); return { x: dataLayers.some(({ xAccessor }) => xAccessor != null) && hasNumberHistogram, yLeft: left.length > 0, yRight: right.length > 0 }; } function getStaticValue(dataLayers, groupId, { activeData }, layerHasNumberHistogram) { const fallbackValue = 100; if (!activeData) { return fallbackValue; } // filter and organize data dimensions into reference layer groups // now pick the columnId in the active data const { dataLayers: filteredLayers, untouchedDataLayers, accessors } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); if (groupId === 'x' && filteredLayers.length && !untouchedDataLayers.some(layerHasNumberHistogram)) { return fallbackValue; } const computedValue = computeStaticValueForGroup(filteredLayers, accessors, activeData, groupId !== 'x', // histogram axis should compute the min based on the current data groupId !== 'x'); return computedValue !== null && computedValue !== void 0 ? computedValue : fallbackValue; } function getAccessorCriteriaForGroup(groupId, dataLayers, activeData) { switch (groupId) { case 'x': { const filteredDataLayers = dataLayers.filter(({ xAccessor }) => xAccessor); // need to reshape the dataLayers to match the other accessors format return { dataLayers: filteredDataLayers.map(({ accessors, xAccessor, ...rest }) => ({ ...rest, accessors: [xAccessor] })), // need the untouched ones to check if there are invalid layers from the filtered ones // to perform the checks the original accessor structure needs to be accessed untouchedDataLayers: filteredDataLayers, accessors: filteredDataLayers.map(({ xAccessor }) => xAccessor) }; } case 'yLeft': case 'yRight': { const prop = groupId === 'yLeft' ? 'left' : 'right'; const { [prop]: axis } = (0, _axes_configuration.groupAxesByType)(dataLayers, activeData); const rightIds = new Set(axis.map(({ layer }) => layer)); const filteredDataLayers = dataLayers.filter(({ layerId }) => rightIds.has(layerId)); return { dataLayers: filteredDataLayers, untouchedDataLayers: filteredDataLayers, accessors: axis.map(({ accessor }) => accessor) }; } } } function computeOverallDataDomain(dataLayers, accessorIds, activeData, allowStacking = true) { const accessorMap = new Set(accessorIds); let min; let max; const [stacked, unstacked] = (0, _lodash.partition)(dataLayers, ({ seriesType }) => (0, _state_helpers.isStackedChart)(seriesType) && allowStacking); for (const { layerId, accessors } of unstacked) { const table = activeData[layerId]; if (table) { for (const accessor of accessors) { if (accessorMap.has(accessor)) { for (const row of table.rows) { const value = row[accessor]; if (typeof value === 'number') { // when not stacked, do not keep the 0 max = max != null ? Math.max(value, max) : value; min = min != null ? Math.min(value, min) : value; } } } } } } // stacked can span multiple layers, so compute an overall max/min by bucket const stackedResults = {}; for (const { layerId, accessors, xAccessor } of stacked) { const table = activeData[layerId]; if (table) { for (const accessor of accessors) { if (accessorMap.has(accessor)) { for (const row of table.rows) { const value = row[accessor]; // start with a shared bucket let bucket = 'shared'; // but if there's an xAccessor use it as new bucket system if (xAccessor) { bucket = row[xAccessor]; } if (typeof value === 'number') { var _stackedResults$bucke; stackedResults[bucket] = (_stackedResults$bucke = stackedResults[bucket]) !== null && _stackedResults$bucke !== void 0 ? _stackedResults$bucke : 0; stackedResults[bucket] += value; } } } } } } for (const value of Object.values(stackedResults)) { // for stacked extents keep 0 in view max = Math.max(value, max || 0, 0); min = Math.min(value, min || 0, 0); } return { min, max }; } function computeStaticValueForGroup(dataLayers, accessorIds, activeData, minZeroOrNegativeBase = true, allowStacking = true) { const defaultReferenceLineFactor = 3 / 4; if (dataLayers.length && accessorIds.length) { if (dataLayers.some(({ seriesType }) => (0, _state_helpers.isPercentageSeries)(seriesType))) { return defaultReferenceLineFactor; } const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData, allowStacking); if (min != null && max != null && isFinite(min) && isFinite(max)) { // Custom axis bounds can go below 0, so consider also lower values than 0 const finalMinValue = minZeroOrNegativeBase ? Math.min(0, min) : min; const interval = max - finalMinValue; return Number((finalMinValue + interval * defaultReferenceLineFactor).toFixed(2)); } } } const getReferenceSupportedLayer = (state, frame) => { const referenceLineGroupIds = [{ id: 'yReferenceLineLeft', label: 'yLeft' }, { id: 'yReferenceLineRight', label: 'yRight' }, { id: 'xReferenceLine', label: 'x' }]; const referenceLineGroups = getGroupsRelatedToData(referenceLineGroupIds, state, (frame === null || frame === void 0 ? void 0 : frame.datasourceLayers) || {}, frame === null || frame === void 0 ? void 0 : frame.activeData); const layers = (state === null || state === void 0 ? void 0 : state.layers) || []; const dataLayers = (0, _visualization_helpers.getDataLayers)(layers); const filledDataLayers = dataLayers.filter(({ accessors, xAccessor }) => accessors.length || xAccessor); const layerHasNumberHistogram = (0, _visualization_helpers.checkScaleOperation)('interval', 'number', (frame === null || frame === void 0 ? void 0 : frame.datasourceLayers) || {}); const initialDimensions = state ? referenceLineGroups.map(({ id, label }) => ({ groupId: id, columnId: (0, _id_generator.generateId)(), dataType: 'number', label: (0, _visualization_helpers.getAxisName)(label, { isHorizontal: (0, _state_helpers.isHorizontalChart)(layers) }), staticValue: getStaticValue(dataLayers, label, { activeData: frame === null || frame === void 0 ? void 0 : frame.activeData }, layerHasNumberHistogram) })) : undefined; return { type: _public.LayerTypes.REFERENCELINE, label: _i18n.i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabel', { defaultMessage: 'Reference lines' }), icon: _chartIcons.IconChartBarReferenceLine, disabled: !filledDataLayers.length || !dataLayers.some(layerHasNumberHistogram) && dataLayers.every(({ accessors }) => !accessors.length), toolTipContent: filledDataLayers.length ? undefined : _i18n.i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp', { defaultMessage: 'Add some data to enable reference layer' }), initialDimensions }; }; exports.getReferenceSupportedLayer = getReferenceSupportedLayer; const setReferenceDimension = ({ prevState, layerId, columnId, groupId, previousColumn }) => { var _newLayer$yConfig, _getReferenceLayers$m; const foundLayer = prevState.layers.find(l => l.layerId === layerId); if (!foundLayer || !(0, _visualization_helpers.isReferenceLayer)(foundLayer)) { return prevState; } const newLayer = { ...foundLayer }; newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; const hasYConfig = (_newLayer$yConfig = newLayer.yConfig) === null || _newLayer$yConfig === void 0 ? void 0 : _newLayer$yConfig.some(({ forAccessor }) => forAccessor === columnId); const previousYConfig = previousColumn ? (_getReferenceLayers$m = (0, _visualization_helpers.getReferenceLayers)(prevState.layers).map(({ yConfig }) => yConfig).flat()) === null || _getReferenceLayers$m === void 0 ? void 0 : _getReferenceLayers$m.find(yConfig => (yConfig === null || yConfig === void 0 ? void 0 : yConfig.forAccessor) === previousColumn) : false; if (!hasYConfig) { const axisMode = groupId === 'xReferenceLine' ? 'bottom' : groupId === 'yReferenceLineRight' ? 'right' : 'left'; newLayer.yConfig = [...(newLayer.yConfig || []), { // override with previous styling, ...previousYConfig, // but keep the new group & id config forAccessor: columnId, axisMode }]; } return { ...prevState, layers: prevState.layers.map(l => l.layerId === layerId ? newLayer : l) }; }; exports.setReferenceDimension = setReferenceDimension; const getSingleColorConfig = (id, color = _color_assignment.defaultReferenceLineColor, icon) => ({ columnId: id, triggerIconType: icon && icon !== 'empty' ? 'custom' : 'color', color, customIcon: icon }); exports.getSingleColorConfig = getSingleColorConfig; const getReferenceLineAccessorColorConfig = layer => { return layer.accessors.map(accessor => { var _layer$yConfig; const currentYConfig = (_layer$yConfig = layer.yConfig) === null || _layer$yConfig === void 0 ? void 0 : _layer$yConfig.find(yConfig => yConfig.forAccessor === accessor); return getSingleColorConfig(accessor, currentYConfig === null || currentYConfig === void 0 ? void 0 : currentYConfig.color); }); }; exports.getReferenceLineAccessorColorConfig = getReferenceLineAccessorColorConfig; const getReferenceConfiguration = ({ state, frame, layer, sortedAccessors }) => { const idToIndex = sortedAccessors.reduce((memo, id, index) => { memo[id] = index; return memo; }, {}); const { bottom, left, right } = (0, _lodash.groupBy)([...(layer.yConfig || [])].sort(({ forAccessor: forA }, { forAccessor: forB }) => idToIndex[forA] - idToIndex[forB]), ({ axisMode }) => { return axisMode; }); const groupsToShow = getGroupsToShow([ // When a reference layer panel is added, a static reference line should automatically be included by default // in the first available axis, in the following order: vertical left, vertical right, horizontal. { config: left, id: 'yReferenceLineLeft', label: 'yLeft', dataTestSubj: 'lnsXY_yReferenceLineLeftPanel' }, { config: right, id: 'yReferenceLineRight', label: 'yRight', dataTestSubj: 'lnsXY_yReferenceLineRightPanel' }, { config: bottom, id: 'xReferenceLine', label: 'x', dataTestSubj: 'lnsXY_xReferenceLinePanel' }], state, frame.datasourceLayers, frame.activeData); const isHorizontal = (0, _state_helpers.isHorizontalChart)(state.layers); return { // Each reference lines layer panel will have sections for each available axis // (horizontal axis, vertical axis left, vertical axis right). // Only axes that support numeric reference lines should be shown groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({ groupId: id, groupLabel: (0, _visualization_helpers.getAxisName)(label, { isHorizontal }), dimensionEditorGroupLabel: _i18n.i18n.translate('xpack.lens.indexPattern.referenceLineDimensionEditorLabel', { defaultMessage: '{groupLabel} reference line', values: { groupLabel: (0, _visualization_helpers.getAxisName)(label, { isHorizontal }) } }), accessors: config.map(({ forAccessor, color, icon }) => getSingleColorConfig(forAccessor, color, icon)), filterOperations: _visualization_helpers.isNumericMetric, supportsMoreColumns: true, requiredMinDimensionCount: 0, enableDimensionEditor: true, supportStaticValue: true, paramEditorCustomProps: { labels: [_i18n.i18n.translate('xpack.lens.indexPattern.staticValue.label', { defaultMessage: 'Reference line value' })], headingLabel: _i18n.i18n.translate('xpack.lens.staticValue.headingLabel', { defaultMessage: 'Placement' }) }, supportFieldFormat: false, dataTestSubj, invalid: !valid, invalidMessage: label === 'x' ? _i18n.i18n.translate('xpack.lens.configure.invalidBottomReferenceLineDimension', { defaultMessage: 'This reference line is assigned to an axis that no longer exists or is no longer valid. You may move this reference line to another available axis or remove it.' }) : _i18n.i18n.translate('xpack.lens.configure.invalidReferenceLineDimension', { defaultMessage: 'This reference line is assigned to an axis that no longer exists. You may move this reference line to another available axis or remove it.' }), requiresPreviousColumnOnDuplicate: true })) }; }; exports.getReferenceConfiguration = getReferenceConfiguration;