From 3eb8164693229d0b7d1ba30900078c072eab1423 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 26 Oct 2021 11:49:37 +0300 Subject: [PATCH 01/39] [WIP][Heatmap] Creates implementation with elastic-charts --- .github/CODEOWNERS | 1 + .i18nrc.json | 1 + src/plugins/vis_types/heatmap/common/index.ts | 10 + src/plugins/vis_types/heatmap/config.ts | 15 + src/plugins/vis_types/heatmap/jest.config.js | 18 + src/plugins/vis_types/heatmap/kibana.json | 14 + .../vis_types/heatmap/public/chart.scss | 18 + .../heatmap/public/editor/collections.ts | 59 +++ .../public/editor/components/heatmap.tsx | 190 +++++++++ .../public/editor/components/index.tsx | 25 ++ .../public/editor/components/labels_panel.tsx | 114 +++++ .../public/editor/components/pie.test.tsx | 138 ++++++ .../heatmap/public/heatmap_component.tsx | 392 ++++++++++++++++++ .../heatmap/public/heatmap_fn.test.ts | 73 ++++ .../vis_types/heatmap/public/heatmap_fn.ts | 255 ++++++++++++ .../heatmap/public/heatmap_renderer.tsx | 63 +++ src/plugins/vis_types/heatmap/public/index.ts | 13 + .../vis_types/heatmap/public/plugin.ts | 74 ++++ .../vis_types/heatmap/public/to_ast.test.ts | 49 +++ .../vis_types/heatmap/public/to_ast.ts | 162 ++++++++ .../vis_types/heatmap/public/to_ast_esaggs.ts | 33 ++ src/plugins/vis_types/heatmap/public/types.ts | 164 ++++++++ .../heatmap/public/vis_type/heatmap.tsx | 141 +++++++ .../heatmap/public/vis_type/index.ts | 14 + src/plugins/vis_types/heatmap/server/index.ts | 17 + .../vis_types/heatmap/server/plugin.ts | 58 +++ src/plugins/vis_types/heatmap/tsconfig.json | 25 ++ src/plugins/vis_types/vislib/kibana.json | 2 +- src/plugins/vis_types/vislib/public/plugin.ts | 7 + .../public/vis_type_vislib_vis_types.ts | 2 - src/plugins/vis_types/vislib/tsconfig.json | 1 + 31 files changed, 2145 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vis_types/heatmap/common/index.ts create mode 100644 src/plugins/vis_types/heatmap/config.ts create mode 100644 src/plugins/vis_types/heatmap/jest.config.js create mode 100644 src/plugins/vis_types/heatmap/kibana.json create mode 100644 src/plugins/vis_types/heatmap/public/chart.scss create mode 100644 src/plugins/vis_types/heatmap/public/editor/collections.ts create mode 100644 src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx create mode 100644 src/plugins/vis_types/heatmap/public/editor/components/index.tsx create mode 100644 src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx create mode 100644 src/plugins/vis_types/heatmap/public/editor/components/pie.test.tsx create mode 100644 src/plugins/vis_types/heatmap/public/heatmap_component.tsx create mode 100644 src/plugins/vis_types/heatmap/public/heatmap_fn.test.ts create mode 100644 src/plugins/vis_types/heatmap/public/heatmap_fn.ts create mode 100644 src/plugins/vis_types/heatmap/public/heatmap_renderer.tsx create mode 100644 src/plugins/vis_types/heatmap/public/index.ts create mode 100644 src/plugins/vis_types/heatmap/public/plugin.ts create mode 100644 src/plugins/vis_types/heatmap/public/to_ast.test.ts create mode 100644 src/plugins/vis_types/heatmap/public/to_ast.ts create mode 100644 src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts create mode 100644 src/plugins/vis_types/heatmap/public/types.ts create mode 100644 src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx create mode 100644 src/plugins/vis_types/heatmap/public/vis_type/index.ts create mode 100644 src/plugins/vis_types/heatmap/server/index.ts create mode 100644 src/plugins/vis_types/heatmap/server/plugin.ts create mode 100644 src/plugins/vis_types/heatmap/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec03acc752d55..771ec080f4b2e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,6 +36,7 @@ /src/plugins/vis_types/vislib/ @elastic/kibana-vis-editors /src/plugins/vis_types/xy/ @elastic/kibana-vis-editors /src/plugins/vis_types/pie/ @elastic/kibana-vis-editors +/src/plugins/vis_types/heatmap/ @elastic/kibana-vis-editors /src/plugins/visualize/ @elastic/kibana-vis-editors /src/plugins/visualizations/ @elastic/kibana-vis-editors /src/plugins/chart_expressions/expression_tagcloud/ @elastic/kibana-vis-editors diff --git a/.i18nrc.json b/.i18nrc.json index 63e4cf6d2fbb9..c17192b725779 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -71,6 +71,7 @@ "visTypeVislib": "src/plugins/vis_types/vislib", "visTypeXy": "src/plugins/vis_types/xy", "visTypePie": "src/plugins/vis_types/pie", + "visTypeHeatmap": "src/plugins/vis_types/heatmap", "visualizations": "src/plugins/visualizations", "visualize": "src/plugins/visualize", "apmOss": "src/plugins/apm_oss", diff --git a/src/plugins/vis_types/heatmap/common/index.ts b/src/plugins/vis_types/heatmap/common/index.ts new file mode 100644 index 0000000000000..6d4ed178a4341 --- /dev/null +++ b/src/plugins/vis_types/heatmap/common/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const DEFAULT_PERCENT_DECIMALS = 2; +export const LEGACY_HEATMAP_CHARTS_LIBRARY = 'visualization:visualize:legacyHeatmapChartsLibrary'; diff --git a/src/plugins/vis_types/heatmap/config.ts b/src/plugins/vis_types/heatmap/config.ts new file mode 100644 index 0000000000000..b831d26854c30 --- /dev/null +++ b/src/plugins/vis_types/heatmap/config.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/heatmap/jest.config.js b/src/plugins/vis_types/heatmap/jest.config.js new file mode 100644 index 0000000000000..6b9df821c48db --- /dev/null +++ b/src/plugins/vis_types/heatmap/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/heatmap'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/heatmap', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_types/heatmap/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/vis_types/heatmap/kibana.json b/src/plugins/vis_types/heatmap/kibana.json new file mode 100644 index 0000000000000..077df9fa1c6ba --- /dev/null +++ b/src/plugins/vis_types/heatmap/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "visTypeHeatmap", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], + "requiredBundles": ["visDefaultEditor"], + "extraPublicDirs": ["common/index"], + "owner": { + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" + }, + "description": "Contains the heatmap implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy heatmap charts library advanced setting." + } diff --git a/src/plugins/vis_types/heatmap/public/chart.scss b/src/plugins/vis_types/heatmap/public/chart.scss new file mode 100644 index 0000000000000..6ba5576300387 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/chart.scss @@ -0,0 +1,18 @@ +.heatmapChart__wrapper, +.heatmapChart__container { + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} + +.heatmapChart__container { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: $euiSizeS; + margin-left: auto; + margin-right: auto; +} diff --git a/src/plugins/vis_types/heatmap/public/editor/collections.ts b/src/plugins/vis_types/heatmap/public/editor/collections.ts new file mode 100644 index 0000000000000..b21b87d0ab91f --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/editor/collections.ts @@ -0,0 +1,59 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; +import { ScaleType } from '../types'; + +export const legendPositions = [ + { + text: i18n.translate('visTypeHeatmap.legendPositions.topText', { + defaultMessage: 'Top', + }), + value: Position.Top, + }, + { + text: i18n.translate('visTypeHeatmap.legendPositions.leftText', { + defaultMessage: 'Left', + }), + value: Position.Left, + }, + { + text: i18n.translate('visTypeHeatmap.legendPositions.rightText', { + defaultMessage: 'Right', + }), + value: Position.Right, + }, + { + text: i18n.translate('visTypeHeatmap.legendPositions.bottomText', { + defaultMessage: 'Bottom', + }), + value: Position.Bottom, + }, +]; + +export const scaleTypes = [ + { + text: i18n.translate('visTypeXy.scaleTypes.linearText', { + defaultMessage: 'Linear', + }), + value: ScaleType.Linear, + }, + { + text: i18n.translate('visTypeXy.scaleTypes.logText', { + defaultMessage: 'Log', + }), + value: ScaleType.Log, + }, + { + text: i18n.translate('visTypeXy.scaleTypes.squareRootText', { + defaultMessage: 'Square root', + }), + value: ScaleType.SquareRoot, + }, +]; diff --git a/src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx new file mode 100644 index 0000000000000..52a9aebf82ce7 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/editor/components/heatmap.tsx @@ -0,0 +1,190 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + BasicOptions, + SelectOption, + SwitchOption, + ColorRanges, + SetColorRangeValue, + SetColorSchemaOptionsValue, + ColorSchemaOptions, + NumberInputOption, + PercentageModeOption, +} from '../../../../../vis_default_editor/public'; +import { colorSchemas } from '../../../../../charts/public'; +import { VisEditorOptionsProps } from '../../../../../visualizations/public'; +// import { PaletteRegistry } from '../../../../../charts/public'; +import { HeatmapVisParams, HeatmapTypeProps, ValueAxis } from '../../types'; +import { LabelsPanel } from './labels_panel'; +import { legendPositions, scaleTypes } from '../collections'; + +export interface HeatmapOptionsProps + extends VisEditorOptionsProps, + HeatmapTypeProps {} + +const HeatmapOptions = (props: HeatmapOptionsProps) => { + const { stateParams, uiState, setValue, setValidity, setTouched } = props; + const [valueAxis] = stateParams.valueAxes; + const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; + const [isColorRangesValid, setIsColorRangesValid] = useState(false); + + const setValueAxisScale = useCallback( + (paramName: T, value: ValueAxis['scale'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + scale: { + ...valueAxis.scale, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + useEffect(() => { + setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid); + }, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]); + + // useEffect(() => { + // const fetchPalettes = async () => { + // const palettes = await props.palettes?.getPalettes(); + // setPalettesRegistry(palettes); + // }; + // fetchPalettes(); + // }, [props.palettes]); + + return ( + <> + + +

+ +

+
+ + + + + +
+ + + + + +

+ +

+
+ + + + + + + + + + + + + + + + + {stateParams.setColorRange && ( + + )} +
+ + + + + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { HeatmapOptions as default }; diff --git a/src/plugins/vis_types/heatmap/public/editor/components/index.tsx b/src/plugins/vis_types/heatmap/public/editor/components/index.tsx new file mode 100644 index 0000000000000..1313d335b06fe --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/editor/components/index.tsx @@ -0,0 +1,25 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { VisEditorOptionsProps } from '../../../../../visualizations/public'; +import { HeatmapVisParams, HeatmapTypeProps } from '../../types'; + +const HeatmapOptionsLazy = lazy(() => import('./heatmap')); + +export const getHeatmapOptions = + ({ showElasticChartsOptions, palettes, trackUiMetric }: HeatmapTypeProps) => + (props: VisEditorOptionsProps) => + ( + + ); diff --git a/src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx b/src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx new file mode 100644 index 0000000000000..5667afb7b8a68 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/editor/components/labels_panel.tsx @@ -0,0 +1,114 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { SwitchOption } from '../../../../../vis_default_editor/public'; +import { HeatmapVisParams, ValueAxis } from '../../types'; + +const VERTICAL_ROTATION = 270; + +interface LabelsPanelProps { + valueAxis: ValueAxis; + setValue: VisEditorOptionsProps['setValue']; +} + +function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { + const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION; + + const setValueAxisLabels = useCallback( + (paramName: T, value: ValueAxis['labels'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + labels: { + ...valueAxis.labels, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + const setRotateLabels = useCallback( + (paramName: 'rotate', value: boolean) => + setValueAxisLabels(paramName, value ? VERTICAL_ROTATION : 0), + [setValueAxisLabels] + ); + + const setColor = useCallback((value) => setValueAxisLabels('color', value), [setValueAxisLabels]); + + return ( + + +

+ +

+
+ + + + + + + + + + + +
+ ); +} + +export { LabelsPanel }; diff --git a/src/plugins/vis_types/heatmap/public/editor/components/pie.test.tsx b/src/plugins/vis_types/heatmap/public/editor/components/pie.test.tsx new file mode 100644 index 0000000000000..ac02b33b92add --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/editor/components/pie.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ReactWrapper } from 'enzyme'; +import PieOptions, { PieOptionsProps } from './pie'; +import { chartPluginMock } from '../../../../../charts/public/mocks'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { act } from 'react-dom/test-utils'; + +describe('PalettePicker', function () { + let props: PieOptionsProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + palettes: chartPluginMock.createSetupContract().palettes, + showElasticChartsOptions: true, + vis: { + type: { + editorConfig: { + collections: { + legendPositions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + }, + }, + }, + }, + stateParams: { + isDonut: true, + legendPosition: 'left', + labels: { + show: true, + }, + }, + setValue: jest.fn(), + } as unknown as PieOptionsProps; + }); + + it('renders the nested legend switch for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieNestedLegendSwitch').length).toBe(1); + }); + }); + + it('not renders the nested legend switch for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieNestedLegendSwitch').length).toBe(0); + }); + }); + + it('renders the long legend options for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'pieLongLegendsOptions').length).toBe(1); + }); + }); + + it('not renders the long legend options for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'pieLongLegendsOptions').length).toBe(0); + }); + }); + + it('renders the label position dropdown for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieLabelPositionSelect').length).toBe(1); + }); + }); + + it('not renders the label position dropdown for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieLabelPositionSelect').length).toBe(0); + }); + }); + + it('renders the top level switch for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieTopLevelSwitch').length).toBe(1); + }); + }); + + it('renders the top level switch for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieTopLevelSwitch').length).toBe(1); + }); + }); + + it('renders the value format dropdown for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieValueFormatsSelect').length).toBe(1); + }); + }); + + it('not renders the value format dropdown for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieValueFormatsSelect').length).toBe(0); + }); + }); + + it('renders the percent slider for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieValueDecimals').length).toBe(1); + }); + }); +}); diff --git a/src/plugins/vis_types/heatmap/public/heatmap_component.tsx b/src/plugins/vis_types/heatmap/public/heatmap_component.tsx new file mode 100644 index 0000000000000..87196451f4a7f --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/heatmap_component.tsx @@ -0,0 +1,392 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react'; + +import { + Chart, + Datum, + LayerValue, + Partition, + Position, + Settings, + RenderChangeListener, + TooltipProps, + TooltipType, + SeriesIdentifier, +} from '@elastic/charts'; +import { + LegendToggle, + ClickTriggerEvent, + ChartsPluginSetup, + PaletteRegistry, +} from '../../../charts/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import type { PersistedState } from '../../../visualizations/public'; +import { + Datatable, + DatatableColumn, + IInterpreterRenderHandlers, +} from '../../../expressions/public'; +import type { FieldFormat } from '../../../field_formats/common'; +import { DEFAULT_PERCENT_DECIMALS } from '../common'; +import { HeatmapVisParams, HeatmapContainerDimensions } from './types'; +// import { PieVisParams, BucketColumns, ValueFormats, PieContainerDimensions } from './types'; +// import { +// getColorPicker, +// getLayers, +// getLegendActions, +// canFilter, +// getFilterClickData, +// getFilterEventData, +// getConfig, +// getColumns, +// getSplitDimensionAccessor, +// } from './utils'; +// import { ChartSplit, SMALL_MULTIPLES_ID } from './components/chart_split'; +// import { VisualizationNoResults } from './components/visualization_noresults'; + +import './chart.scss'; + +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + +export interface HeatmapComponentProps { + visParams: HeatmapVisParams; + visData: Datatable; + uiState: PersistedState; + fireEvent: IInterpreterRenderHandlers['event']; + renderComplete: IInterpreterRenderHandlers['done']; + chartsThemeService: ChartsPluginSetup['theme']; + palettesRegistry: PaletteRegistry; + services: DataPublicPluginStart; + syncColors: boolean; +} + +const HeatmapComponent = (props: HeatmapComponentProps) => { + const chartTheme = props.chartsThemeService.useChartsTheme(); + const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme(); + // const [showLegend, setShowLegend] = useState(() => { + // const bwcLegendStateDefault = + // props.visParams.addLegend == null ? false : props.visParams.addLegend; + // return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; + // }); + const [dimensions, setDimensions] = useState(); + + const parentRef = useRef(null); + + useEffect(() => { + if (parentRef && parentRef.current) { + const parentHeight = parentRef.current!.getBoundingClientRect().height; + const parentWidth = parentRef.current!.getBoundingClientRect().width; + setDimensions({ width: parentWidth, height: parentHeight }); + } + }, [parentRef]); + + const onRenderChange = useCallback( + (isRendered) => { + if (isRendered) { + props.renderComplete(); + } + }, + [props] + ); + + // handles slice click event + // const handleSliceClick = useCallback( + // ( + // clickedLayers: LayerValue[], + // bucketColumns: Array>, + // visData: Datatable, + // splitChartDimension?: DatatableColumn, + // splitChartFormatter?: FieldFormat + // ): void => { + // const data = getFilterClickData( + // clickedLayers, + // bucketColumns, + // visData, + // splitChartDimension, + // splitChartFormatter + // ); + // const event = { + // name: 'filterBucket', + // data: { data }, + // }; + // props.fireEvent(event); + // }, + // [props] + // ); + + // handles legend action event data + // const getLegendActionEventData = useCallback( + // (visData: Datatable) => + // (series: SeriesIdentifier): ClickTriggerEvent | null => { + // const data = getFilterEventData(visData, series); + + // return { + // name: 'filterBucket', + // data: { + // negate: false, + // data, + // }, + // }; + // }, + // [] + // ); + + // const handleLegendAction = useCallback( + // (event: ClickTriggerEvent, negate = false) => { + // props.fireEvent({ + // ...event, + // data: { + // ...event.data, + // negate, + // }, + // }); + // }, + // [props] + // ); + + // const toggleLegend = useCallback(() => { + // setShowLegend((value) => { + // const newValue = !value; + // props.uiState?.set('vis.legendOpen', newValue); + // return newValue; + // }); + // }, [props.uiState]); + + // useEffect(() => { + // setShowLegend(props.visParams.addLegend); + // props.uiState?.set('vis.legendOpen', props.visParams.addLegend); + // }, [props.uiState, props.visParams.addLegend]); + + // const setColor = useCallback( + // (newColor: string | null, seriesLabel: string | number) => { + // const colors = props.uiState?.get('vis.colors') || {}; + // if (colors[seriesLabel] === newColor || !newColor) { + // delete colors[seriesLabel]; + // } else { + // colors[seriesLabel] = newColor; + // } + // props.uiState?.setSilent('vis.colors', null); + // props.uiState?.set('vis.colors', colors); + // props.uiState?.emit('reload'); + // }, + // [props.uiState] + // ); + + // const { visData, visParams, services, syncColors } = props; + + // function getSliceValue(d: Datum, metricColumn: DatatableColumn) { + // const value = d[metricColumn.id]; + // return Number.isFinite(value) && value >= 0 ? value : 0; + // } + + // formatters + // const metricFieldFormatter = services.fieldFormats.deserialize( + // visParams.dimensions.metric.format + // ); + // const splitChartFormatter = visParams.dimensions.splitColumn + // ? services.fieldFormats.deserialize(visParams.dimensions.splitColumn[0].format) + // : visParams.dimensions.splitRow + // ? services.fieldFormats.deserialize(visParams.dimensions.splitRow[0].format) + // : undefined; + // const percentFormatter = services.fieldFormats.deserialize({ + // id: 'percent', + // params: { + // pattern: `0,0.[${'0'.repeat(visParams.labels.percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`, + // }, + // }); + + // const { bucketColumns, metricColumn } = useMemo( + // () => getColumns(visParams, visData), + // [visData, visParams] + // ); + + // const layers = useMemo( + // () => + // getLayers( + // bucketColumns, + // visParams, + // props.uiState?.get('vis.colors', {}), + // visData.rows, + // props.palettesRegistry, + // services.fieldFormats, + // syncColors + // ), + // [ + // bucketColumns, + // visParams, + // props.uiState, + // props.palettesRegistry, + // visData.rows, + // services.fieldFormats, + // syncColors, + // ] + // ); + // const config = useMemo( + // () => getConfig(visParams, chartTheme, dimensions), + // [chartTheme, visParams, dimensions] + // ); + // const tooltip: TooltipProps = { + // type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None, + // }; + // const legendPosition = visParams.legendPosition ?? Position.Right; + + // const legendColorPicker = useMemo( + // () => + // getColorPicker( + // legendPosition, + // setColor, + // bucketColumns, + // visParams.palette.name, + // visData.rows, + // props.uiState, + // visParams.distinctColors + // ), + // [ + // legendPosition, + // setColor, + // bucketColumns, + // visParams.palette.name, + // visParams.distinctColors, + // visData.rows, + // props.uiState, + // ] + // ); + + // const splitChartColumnAccessor = visParams.dimensions.splitColumn + // ? getSplitDimensionAccessor( + // services.fieldFormats, + // visData.columns + // )(visParams.dimensions.splitColumn[0]) + // : undefined; + // const splitChartRowAccessor = visParams.dimensions.splitRow + // ? getSplitDimensionAccessor( + // services.fieldFormats, + // visData.columns + // )(visParams.dimensions.splitRow[0]) + // : undefined; + + // const splitChartDimension = visParams.dimensions.splitColumn + // ? visData.columns[visParams.dimensions.splitColumn[0].accessor] + // : visParams.dimensions.splitRow + // ? visData.columns[visParams.dimensions.splitRow[0].accessor] + // : undefined; + + // /** + // * Checks whether data have all zero values. + // * If so, the no data container is loaded. + // */ + // const isAllZeros = useMemo( + // () => visData.rows.every((row) => row[metricColumn.id] === 0), + // [visData.rows, metricColumn] + // ); + + // /** + // * Checks whether data have negative values. + // * If so, the no data container is loaded. + // */ + // const hasNegative = useMemo( + // () => + // visData.rows.some((row) => { + // const value = row[metricColumn.id]; + // return typeof value === 'number' && value < 0; + // }), + // [visData.rows, metricColumn] + // ); + + return ( +
+
+ Miaouuuuu + {/* + + + { + handleSliceClick( + args[0][0] as LayerValue[], + bucketColumns, + visData, + splitChartDimension, + splitChartFormatter + ); + }} + legendAction={getLegendActions( + canFilter, + getLegendActionEventData(visData), + handleLegendAction, + visParams, + services.actions, + services.fieldFormats + )} + theme={[ + chartTheme, + { + legend: { + labelOptions: { + maxLines: visParams.truncateLegend ? visParams.maxLegendLines ?? 1 : 0, + }, + }, + }, + ]} + baseTheme={chartBaseTheme} + onRenderChange={onRenderChange} + /> + getSliceValue(d, metricColumn)} + percentFormatter={(d: number) => percentFormatter.convert(d / 100)} + valueGetter={ + !visParams.labels.show || + visParams.labels.valuesFormat === ValueFormats.VALUE || + !visParams.labels.values + ? undefined + : 'percent' + } + valueFormatter={(d: number) => + !visParams.labels.show || !visParams.labels.values + ? '' + : metricFieldFormatter.convert(d) + } + layers={layers} + config={config} + topGroove={!visParams.labels.show ? 0 : undefined} + /> + */} +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default memo(HeatmapComponent); diff --git a/src/plugins/vis_types/heatmap/public/heatmap_fn.test.ts b/src/plugins/vis_types/heatmap/public/heatmap_fn.test.ts new file mode 100644 index 0000000000000..9ba21cdc847e5 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/heatmap_fn.test.ts @@ -0,0 +1,73 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; +import { createPieVisFn } from './pie_fn'; +import { PieVisConfig } from './types'; +import { Datatable } from '../../../expressions/common/expression_types/specs'; + +describe('interpreter/functions#pie', () => { + const fn = functionWrapper(createPieVisFn()); + const context = { + type: 'datatable', + rows: [{ 'col-0-1': 0 }], + columns: [{ id: 'col-0-1', name: 'Count' }], + } as unknown as Datatable; + const visConfig = { + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + nestedLegend: true, + truncateLegend: true, + maxLegendLines: true, + distinctColors: false, + palette: 'kibana_palette', + labels: { + show: false, + values: true, + position: 'default', + valuesFormat: 'percent', + percentDecimals: 2, + truncate: 100, + }, + metric: { + accessor: 0, + format: { + id: 'number', + }, + params: {}, + aggType: 'count', + }, + } as unknown as PieVisConfig; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns an object with the correct structure', async () => { + const actual = await fn(context, visConfig); + expect(actual).toMatchSnapshot(); + }); + + it('logs correct datatable to inspector', async () => { + let loggedTable: Datatable; + const handlers = { + inspectorAdapters: { + tables: { + logDatatable: (name: string, datatable: Datatable) => { + loggedTable = datatable; + }, + }, + }, + }; + await fn(context, visConfig, handlers as any); + + expect(loggedTable!).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_types/heatmap/public/heatmap_fn.ts b/src/plugins/vis_types/heatmap/public/heatmap_fn.ts new file mode 100644 index 0000000000000..8fbf2bb6cfac9 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/heatmap_fn.ts @@ -0,0 +1,255 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueRender, +} from '../../../expressions/common'; +import { HeatmapVisConfig, HeatmapVisParams } from './types'; +import { prepareLogTable, Dimension } from '../../../visualizations/public'; + +export const vislibHeatmapName = 'heatmap_vis'; + +export interface HeatmapRendererConfig { + visData: Datatable; + visType: string; + visConfig: HeatmapVisParams; + syncColors: boolean; +} + +export type ExpressionHeatmapFunction = ExpressionFunctionDefinition< + typeof vislibHeatmapName, + Datatable, + HeatmapVisConfig, + ExpressionValueRender +>; +export const createHeatmapVisFn = (): ExpressionHeatmapFunction => ({ + name: vislibHeatmapName, + type: 'render', + context: { + types: ['datatable'], + }, + help: i18n.translate('visTypeHeatmap.functions.help', { + defaultMessage: 'Heatmap visualization', + }), + args: { + xDimension: { + types: ['xy_dimension', 'null'], + help: i18n.translate('visTypeHeatmap.function.args.xDimension.help', { + defaultMessage: 'X axis dimension config', + }), + }, + yDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.yDimension.help', { + defaultMessage: 'Y axis dimension config', + }), + multi: true, + }, + zDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.zDimension.help', { + defaultMessage: 'Z axis dimension config', + }), + multi: true, + }, + widthDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.widthDimension.help', { + defaultMessage: 'Width dimension config', + }), + multi: true, + }, + seriesDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.seriesDimension.help', { + defaultMessage: 'Series dimension config', + }), + multi: true, + }, + splitRowDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.splitRowDimension.help', { + defaultMessage: 'Split by row dimension config', + }), + multi: true, + }, + splitColumnDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeHeatmap.function.args.splitColumnDimension.help', { + defaultMessage: 'Split by column dimension config', + }), + multi: true, + }, + addTooltip: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.addTooltipHelpText', { + defaultMessage: 'Show tooltip on hover', + }), + default: true, + }, + invertColors: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.invertColorsHelpText', { + defaultMessage: 'TBD', + }), + default: false, + }, + addLegend: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.addLegendHelpText', { + defaultMessage: 'Show legend chart legend', + }), + }, + enableHover: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.enableHoverHelpText', { + defaultMessage: 'Enables hover', + }), + }, + legendPosition: { + types: ['string'], + help: i18n.translate('visTypeHeatmap.function.args.legendPositionHelpText', { + defaultMessage: 'Position the legend on top, bottom, left, right of the chart', + }), + }, + colorsNumber: { + types: ['number'], + help: i18n.translate('visTypeHeatmap.function.args.colorsNumberHelpText', { + defaultMessage: 'TBD', + }), + }, + colorSchema: { + types: ['string'], + help: i18n.translate('visTypeHeatmap.function.args.colorSchemaHelpText', { + defaultMessage: 'TBD', + }), + }, + setColorRange: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.setColorRangeHelpText', { + defaultMessage: 'TBD', + }), + }, + percentageMode: { + types: ['boolean'], + help: i18n.translate('visTypeHeatmap.function.args.percentageModeHelpText', { + defaultMessage: 'TBD', + }), + }, + percentageFormatPattern: { + types: ['string'], + help: i18n.translate('visTypeHeatmap.function.args.percentageFormatPatternHelpText', { + defaultMessage: 'TBD', + }), + }, + valueAxes: { + types: ['value_axis'], + help: i18n.translate('visTypeHeatmap.function.args.valueAxes.help', { + defaultMessage: 'Value axis config', + }), + multi: true, + }, + categoryAxes: { + types: ['category_axis'], + help: i18n.translate('visTypeHeatmap.function.args.categoryAxes.help', { + defaultMessage: 'Category axis config', + }), + multi: true, + }, + // maxLegendLines: { + // types: ['number'], + // help: i18n.translate('visTypeHeatmap.function.args.maxLegendLinesHelpText', { + // defaultMessage: 'Defines the number of lines per legend item', + // }), + // }, + palette: { + types: ['palette', 'system_palette'], + help: i18n.translate('visTypeHeatmap.function.args.paletteHelpText', { + defaultMessage: 'Defines the chart palette', + }), + default: '{palette}', + }, + }, + fn(context, args, handlers) { + const visConfig = { + ...args, + valueAxes: args.valueAxes.map((valueAxis) => ({ ...valueAxis, type: valueAxis.axisType })), + categoryAxes: args.categoryAxes.map((categoryAxis) => ({ + ...categoryAxis, + type: categoryAxis.axisType, + })), + dimensions: { + x: args.xDimension, + y: args.yDimension, + z: args.zDimension, + width: args.widthDimension, + series: args.seriesDimension, + splitRow: args.splitRowDimension, + splitColumn: args.splitColumnDimension, + }, + } as HeatmapVisParams; + + if (handlers?.inspectorAdapters?.tables) { + const argsTable: Dimension[] = [ + [ + args.yDimension, + i18n.translate('visTypeHeatmap.function.dimension.metric', { + defaultMessage: 'Metric', + }), + ], + [ + args.zDimension, + i18n.translate('visTypeHeatmap.function.adimension.dotSize', { + defaultMessage: 'Dot size', + }), + ], + [ + args.splitColumnDimension, + i18n.translate('visTypeHeatmap.function.dimension.splitcolumn', { + defaultMessage: 'Column split', + }), + ], + [ + args.splitRowDimension, + i18n.translate('visTypeHeatmap.function.dimension.splitrow', { + defaultMessage: 'Row split', + }), + ], + ]; + + if (args.xDimension) { + argsTable.push([ + [args.xDimension], + i18n.translate('visTypeHeatmap.function.adimension.bucket', { + defaultMessage: 'Bucket', + }), + ]); + } + + const logTable = prepareLogTable(context, argsTable); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: vislibHeatmapName, + value: { + visData: context, + visConfig, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + visType: 'heatmap', + params: { + listenOnChange: true, + }, + }, + }; + }, +}); diff --git a/src/plugins/vis_types/heatmap/public/heatmap_renderer.tsx b/src/plugins/vis_types/heatmap/public/heatmap_renderer.tsx new file mode 100644 index 0000000000000..11b0ee9efb372 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/heatmap_renderer.tsx @@ -0,0 +1,63 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ExpressionRenderDefinition } from '../../../expressions/public'; +import { VisualizationContainer } from '../../../visualizations/public'; +import type { PersistedState } from '../../../visualizations/public'; +import { VisTypeHeatmapDependencies } from './plugin'; + +import { HeatmapRendererConfig, vislibHeatmapName } from './heatmap_fn'; + +const HeatmapComponent = lazy(() => import('./heatmap_component')); + +function shouldShowNoResultsMessage(visData: any): boolean { + const rows: object[] | undefined = visData?.rows; + const isZeroHits = visData?.hits === 0 || (rows && !rows.length); + + return Boolean(isZeroHits); +} + +export const getHeatmapVisRenderer: ( + deps: VisTypeHeatmapDependencies +) => ExpressionRenderDefinition = ({ theme, palettes, getStartDeps }) => ({ + name: vislibHeatmapName, + displayName: 'Heatmap visualization', + reuseDomNode: true, + render: async (domNode, { visConfig, visData, syncColors }, handlers) => { + const showNoResult = shouldShowNoResultsMessage(visData); + + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const services = await getStartDeps(); + const palettesRegistry = await palettes.getPalettes(); + + render( + + + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_types/heatmap/public/index.ts b/src/plugins/vis_types/heatmap/public/index.ts new file mode 100644 index 0000000000000..6b2657ffc4f0b --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/index.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VisTypePiePlugin } from './plugin'; + +export { heatmapVisType } from './vis_type'; + +export const plugin = () => new VisTypePiePlugin(); diff --git a/src/plugins/vis_types/heatmap/public/plugin.ts b/src/plugins/vis_types/heatmap/public/plugin.ts new file mode 100644 index 0000000000000..be1211ad9b69c --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/plugin.ts @@ -0,0 +1,74 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, DocLinksStart } from 'src/core/public'; +import { VisualizationsSetup } from '../../../visualizations/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; +import { ChartsPluginSetup } from '../../../charts/public'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { LEGACY_HEATMAP_CHARTS_LIBRARY } from '../common'; +import { createHeatmapVisFn } from './heatmap_fn'; +import { getHeatmapVisRenderer } from './heatmap_renderer'; +import { heatmapVisType } from './vis_type'; + +/** @internal */ +export interface VisTypeHeatmapSetupDependencies { + visualizations: VisualizationsSetup; + expressions: ReturnType; + charts: ChartsPluginSetup; + usageCollection: UsageCollectionSetup; +} + +/** @internal */ +export interface VisTypeHeatmapPluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export interface VisTypeHeatmapDependencies { + theme: ChartsPluginSetup['theme']; + palettes: ChartsPluginSetup['palettes']; + getStartDeps: () => Promise<{ data: DataPublicPluginStart; docLinks: DocLinksStart }>; +} + +export class VisTypePiePlugin { + setup( + core: CoreSetup, + { expressions, visualizations, charts, usageCollection }: VisTypeHeatmapSetupDependencies + ) { + if (!core.uiSettings.get(LEGACY_HEATMAP_CHARTS_LIBRARY, false)) { + const getStartDeps = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + data: deps.data, + docLinks: coreStart.docLinks, + }; + }; + const trackUiMetric = usageCollection?.reportUiCounter.bind( + usageCollection, + 'vis_type_heatmap' + ); + + expressions.registerFunction(createHeatmapVisFn); + expressions.registerRenderer( + getHeatmapVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps }) + ); + visualizations.createBaseVisualization( + heatmapVisType({ + showElasticChartsOptions: true, + palettes: charts.palettes, + trackUiMetric, + }) + ); + } + return {}; + } + + start() {} +} diff --git a/src/plugins/vis_types/heatmap/public/to_ast.test.ts b/src/plugins/vis_types/heatmap/public/to_ast.test.ts new file mode 100644 index 0000000000000..70a1f938a8266 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/to_ast.test.ts @@ -0,0 +1,49 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Vis } from '../../../visualizations/public'; +import { buildExpression } from '../../../expressions/public'; + +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; +import { sampleAreaVis } from '../../xy/public/sample_vis.test.mocks'; + +jest.mock('../../../expressions/public', () => ({ + ...(jest.requireActual('../../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('vislib vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = sampleAreaVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_types/heatmap/public/to_ast.ts b/src/plugins/vis_types/heatmap/public/to_ast.ts new file mode 100644 index 0000000000000..ccee7c389a487 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/to_ast.ts @@ -0,0 +1,162 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; + +import { VisToExpressionAst, getVisSchemas } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import type { DateHistogramParams, HistogramParams } from '../../../visualizations/public'; +import type { Labels } from '../../../charts/public'; + +import { BUCKET_TYPES } from '../../../data/public'; + +import { vislibHeatmapName, ExpressionHeatmapFunction } from './heatmap_fn'; +import type { + Dimensions, + Dimension, + ValueAxis, + CategoryAxis, + Scale, + HeatmapVisParams, +} from './types'; +import { getEsaggsFn } from './to_ast_esaggs'; + +const prepareVisDimension = (data: Dimension) => { + const visDimension = buildExpressionFunction('visdimension', { accessor: data.accessor }); + + if (data.format) { + visDimension.addArgument('format', data.format.id); + visDimension.addArgument('formatParams', JSON.stringify(data.format.params)); + } + + return buildExpression([visDimension]); +}; + +const prepareXYDimension = (data: Dimension) => { + const xyDimension = buildExpressionFunction('xydimension', { + params: JSON.stringify(data.params), + aggType: data.aggType, + label: data.label, + visDimension: prepareVisDimension(data), + }); + + return buildExpression([xyDimension]); +}; + +const prepareScale = (data: Scale) => { + const scale = buildExpressionFunction('visscale', { + ...data, + }); + + return buildExpression([scale]); +}; + +const prepareLabel = (data: Labels) => { + const label = buildExpressionFunction('label', { + ...data, + }); + + return buildExpression([label]); +}; + +const prepareCategoryAxis = (data: CategoryAxis) => { + const categoryAxis = buildExpressionFunction('categoryaxis', { + id: data.id, + show: data.show, + position: data?.position ?? 'bottom', + type: data.type, + title: data?.title?.text, + scale: prepareScale(data.scale), + labels: prepareLabel(data.labels), + }); + + return buildExpression([categoryAxis]); +}; + +const prepareValueAxis = (data: ValueAxis) => { + const categoryAxis = buildExpressionFunction('valueaxis', { + name: data.id, + axisParams: prepareCategoryAxis({ + ...data, + }), + }); + + return buildExpression([categoryAxis]); +}; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const dimensions: Dimensions = { + x: schemas.segment ? schemas.segment[0] : null, + y: schemas.metric, + z: schemas.radius, + width: schemas.width, + series: schemas.group, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const responseAggs = vis.data.aggs?.getResponseAggs() ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === BUCKET_TYPES.DATE_HISTOGRAM) { + (dimensions.x.params as DateHistogramParams).date = true; + const { esUnit, esValue } = xAgg.buckets.getInterval(); + (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; + (dimensions.x.params as DateHistogramParams).intervalESValue = esValue; + (dimensions.x.params as DateHistogramParams).interval = moment + .duration(esValue, esUnit) + .asMilliseconds(); + (dimensions.x.params as DateHistogramParams).format = xAgg.buckets.getScaledDateFormat(); + (dimensions.x.params as DateHistogramParams).bounds = xAgg.buckets.getBounds(); + } else if (xAgg.type.name === BUCKET_TYPES.HISTOGRAM) { + const intervalParam = xAgg.type.paramByName('interval'); + const output = { params: {} as any }; + await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { + abortSignal: params.abortSignal, + }); + intervalParam.write(xAgg, output); + (dimensions.x.params as HistogramParams).interval = output.params.interval; + } + } + + const args = { + // explicitly pass each param to prevent extra values trapping + addTooltip: vis.params.addTooltip, + invertColors: vis.params.invertColors, + enableHover: vis.params.enableHover, + addLegend: vis.params.addLegend, + legendPosition: vis.params.legendPosition, + colorsNumber: vis.params.colorsNumber, + colorSchema: vis.params.colorSchema, + setColorRange: vis.params.setColorRange, + percentageMode: vis.params.percentageMode, + percentageFormatPattern: vis.params.percentageFormatPattern, + valueAxes: vis.params.valueAxes.map(prepareValueAxis), + categoryAxes: vis.params.categoryAxes.map(prepareCategoryAxis), + // maxLegendLines: vis.params.maxLegendLines, + palette: vis.params?.palette, + xDimension: dimensions.x ? prepareXYDimension(dimensions.x) : null, + yDimension: dimensions.y.map(prepareXYDimension), + zDimension: dimensions.z?.map(prepareXYDimension), + widthDimension: dimensions.width?.map(prepareXYDimension), + seriesDimension: dimensions.series?.map(prepareXYDimension), + splitRowDimension: dimensions.splitRow?.map(prepareXYDimension), + splitColumnDimension: dimensions.splitColumn?.map(prepareXYDimension), + }; + + const visTypeHeatmap = buildExpressionFunction( + vislibHeatmapName, + args + ); + + const ast = buildExpression([getEsaggsFn(vis), visTypeHeatmap]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts b/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts new file mode 100644 index 0000000000000..9b6e02928f7a9 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts @@ -0,0 +1,33 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Vis } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import { + EsaggsExpressionFunctionDefinition, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../../data/public'; + +import { HeatmapVisParams } from './types'; + +/** + * Get esaggs expressions function + * @param vis + */ +export function getEsaggsFn(vis: Vis) { + return buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction('indexPatternLoad', { + id: vis.data.indexPattern!.id!, + }), + ]), + metricsAtAllLevels: vis.isHierarchical(), + partialRows: false, + aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }); +} diff --git a/src/plugins/vis_types/heatmap/public/types.ts b/src/plugins/vis_types/heatmap/public/types.ts new file mode 100644 index 0000000000000..d55423adc3868 --- /dev/null +++ b/src/plugins/vis_types/heatmap/public/types.ts @@ -0,0 +1,164 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { UiCounterMetricType } from '@kbn/analytics'; +import type { Position } from '@elastic/charts'; +import type { PaletteOutput, ChartsPluginSetup, Style, Labels } from '../../../charts/public'; +import type { ExpressionValueBoxed } from '../../../expressions/public'; +import { RangeValues } from '../../../vis_default_editor/public'; +import type { + SchemaConfig, + FakeParams, + HistogramParams, + DateHistogramParams, + ExpressionValueXYDimension, +} from '../../../visualizations/public'; + +export interface HeatmapTypeProps { + showElasticChartsOptions?: boolean; + palettes?: ChartsPluginSetup['palettes']; + trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; +} + +interface HeatmapCommonParams { + addLegend: boolean; + addTooltip: boolean; + enableHover: boolean; + legendPosition: Position; + colorsNumber: number | ''; + invertColors: boolean; + // colorsRange: RangeValues[]; + colorSchema: string; + setColorRange: boolean; + percentageMode: boolean; + percentageFormatPattern?: string; +} + +export interface HeatmapVisConfig extends HeatmapCommonParams { + xDimension: ExpressionValueXYDimension | null; + yDimension: ExpressionValueXYDimension[]; + zDimension?: ExpressionValueXYDimension[]; + widthDimension?: ExpressionValueXYDimension[]; + seriesDimension?: ExpressionValueXYDimension[]; + splitRowDimension?: ExpressionValueXYDimension[]; + splitColumnDimension?: ExpressionValueXYDimension[]; + palette: PaletteOutput; + valueAxes: ExpressionValueValueAxis[]; + categoryAxes: ExpressionValueCategoryAxis[]; +} + +export interface HeatmapVisParams extends HeatmapCommonParams { + dimensions: Dimensions; + palette: PaletteOutput; + valueAxes: ValueAxis[]; + categoryAxes: CategoryAxis[]; +} +// ToDo: move them to constants +export enum ScaleType { + Linear = 'linear', + Log = 'log', + SquareRoot = 'square root', +} + +export enum AxisType { + Category = 'category', + Value = 'value', +} +export enum AxisMode { + Normal = 'normal', + Percentage = 'percentage', + Wiggle = 'wiggle', + Silhouette = 'silhouette', +} + +export enum ChartMode { + Normal = 'normal', + Stacked = 'stacked', +} + +export enum InterpolationMode { + Linear = 'linear', + Cardinal = 'cardinal', + StepAfter = 'step-after', +} + +export interface Scale { + boundsMargin?: number | ''; + defaultYExtents?: boolean; + max?: number | null; + min?: number | null; + mode?: AxisMode; + setYExtents?: boolean; + type: ScaleType; +} + +type ExpressionValueCategoryAxis = ExpressionValueBoxed< + 'category_axis', + { + id: CategoryAxis['id']; + show: CategoryAxis['show']; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +type ExpressionValueValueAxis = ExpressionValueBoxed< + 'value_axis', + { + name: string; + id: string; + show: boolean; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +export interface CategoryAxis { + id: string; + labels: Labels; + position: Position; + scale: Scale; + show: boolean; + title?: { + text?: string; + }; + type: AxisType; + style?: Partial