diff --git a/config/kibana.yml b/config/kibana.yml index 72e0764f849a0..5f3206c3a65ef 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -1,6 +1,8 @@ # Kibana is served by a back end server. This setting specifies the port to use. #server.port: 5601 +vis_type_xy.enabled: true + # Specifies the address to which the Kibana server will bind. IP addresses and host names are both valid values. # The default is 'localhost', which usually means remote machines will not be able to connect. # To allow connections from remote users, set this parameter to a non-loopback address. diff --git a/package.json b/package.json index 1f2749ea44a90..349b56709ebe4 100644 --- a/package.json +++ b/package.json @@ -226,7 +226,7 @@ "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.5.0", - "@elastic/charts": "21.1.2", + "@elastic/charts": "21.3.2", "@elastic/ems-client": "7.9.3", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 372126c4418f5..a0dbd5a472bcd 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "21.1.2", + "@elastic/charts": "21.3.2", "@elastic/eui": "29.0.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/src/plugins/charts/public/services/colors/colors.ts b/src/plugins/charts/public/services/colors/colors.ts index 7a1ffc433ee87..ccd0673b576a0 100644 --- a/src/plugins/charts/public/services/colors/colors.ts +++ b/src/plugins/charts/public/services/colors/colors.ts @@ -48,7 +48,7 @@ export class ColorsService { } createColorLookupFunction( - arrayOfStringsOrNumbers?: any, + arrayOfStringsOrNumbers?: Array, colorMapping: Partial> = {} ) { if (!Array.isArray(arrayOfStringsOrNumbers)) { @@ -67,7 +67,7 @@ export class ColorsService { this.mappedColors.mapKeys(arrayOfStringsOrNumbers); - return (value: string) => { + return (value: string | number) => { return colorMapping[value] || this.mappedColors.get(value); }; } diff --git a/src/plugins/charts/public/services/colors/mapped_colors.ts b/src/plugins/charts/public/services/colors/mapped_colors.ts index 3b9e1501d638d..c99a1783ba36b 100644 --- a/src/plugins/charts/public/services/colors/mapped_colors.ts +++ b/src/plugins/charts/public/services/colors/mapped_colors.ts @@ -45,10 +45,6 @@ export class MappedColors { return _.mapValues(this.uiSettings.get(COLOR_MAPPING_SETTING), standardizeColor); } - public get oldMap(): any { - return this._oldMap; - } - public get mapping(): any { return this._mapping; } @@ -57,16 +53,6 @@ export class MappedColors { return this.getConfigColorMapping()[key as any] || this._mapping[key]; } - flush() { - this._oldMap = _.clone(this._mapping); - this._mapping = {}; - } - - purge() { - this._oldMap = {}; - this._mapping = {}; - } - mapKeys(keys: Array) { const configMapping = this.getConfigColorMapping(); const configColors = _.values(configMapping); diff --git a/src/plugins/charts/public/static/components/_legend_toggle.scss b/src/plugins/charts/public/static/components/_legend_toggle.scss new file mode 100644 index 0000000000000..f418922ded2d8 --- /dev/null +++ b/src/plugins/charts/public/static/components/_legend_toggle.scss @@ -0,0 +1,30 @@ +.legend__toggle { + border-radius: $euiBorderRadius; + position: absolute; + bottom: 0; + left: 0; + display: flex; + padding: $euiSizeXS; + background-color: $euiColorEmptyShade; + transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; + z-index: 1; + margin: $euiSizeXS; + + &:focus { + @include euiFocusRing(); + } + + &--isOpen { + background-color: transparentize($euiColorDarkestShade, .9); + opacity: 1; + } + + &--position-left, + &--position-bottom { + left: auto; + bottom: auto; + right: 0; + top: 0; + } +} + diff --git a/src/plugins/charts/public/static/components/basic_options.tsx b/src/plugins/charts/public/static/components/basic_options.tsx index cac4c8d70d796..e6d102d12cb3b 100644 --- a/src/plugins/charts/public/static/components/basic_options.tsx +++ b/src/plugins/charts/public/static/components/basic_options.tsx @@ -18,15 +18,18 @@ */ import React from 'react'; + import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; + +import { VisOptionsProps } from '../../../../vis_default_editor/public'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { SwitchOption } from './switch'; import { SelectOption } from './select'; interface BasicOptionsParams { addTooltip: boolean; - legendPosition: string; + legendPosition: Position; } function BasicOptions({ diff --git a/src/plugins/charts/public/static/components/collections.ts b/src/plugins/charts/public/static/components/collections.ts index 9dde50f2b44c9..16d875836a202 100644 --- a/src/plugins/charts/public/static/components/collections.ts +++ b/src/plugins/charts/public/static/components/collections.ts @@ -18,17 +18,22 @@ */ import { $Values } from '@kbn/utility-types'; +import { i18n } from '@kbn/i18n'; -export const ColorModes = Object.freeze({ - BACKGROUND: 'Background' as 'Background', - LABELS: 'Labels' as 'Labels', - NONE: 'None' as 'None', +export const ColorMode = Object.freeze({ + Background: 'Background' as 'Background', + Labels: 'Labels' as 'Labels', + None: 'None' as 'None', }); -export type ColorModes = $Values; +export type ColorMode = $Values; -export const Rotates = Object.freeze({ - HORIZONTAL: 0, - VERTICAL: 90, - ANGLED: 75, +export const LabelRotation = Object.freeze({ + Horizontal: 0, + Vertical: 90, + Angled: 75, +}); +export type LabelRotation = $Values; + +export const defaultCountLabel = i18n.translate('charts.countText', { + defaultMessage: 'Count', }); -export type Rotates = $Values; diff --git a/src/plugins/charts/public/static/components/color_picker.tsx b/src/plugins/charts/public/static/components/color_picker.tsx new file mode 100644 index 0000000000000..12b03f2885b6a --- /dev/null +++ b/src/plugins/charts/public/static/components/color_picker.tsx @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React from 'react'; + +import { EuiIcon } from '@elastic/eui'; + +export const legendColors: string[] = [ + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#508642', + '#CCA300', + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#7EB26D', + '#EAB839', + '#6ED0E0', + '#EF843C', + '#E24D42', + '#1F78C1', + '#BA43A9', + '#705DA0', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +interface ColorPickerProps { + onChange: (color: string) => void; + color: string; +} + +export const ColorPicker = ({ onChange, color: selectedColor }: ColorPickerProps) => ( +
+
+ {legendColors.map((color) => ( + onChange(color)} + onKeyPress={() => onChange(color)} + className={classNames('visLegend__valueColorPickerDot', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'visLegend__valueColorPickerDot-isSelected': color === selectedColor, + })} + style={{ color }} + data-test-subj={`legendSelectColor-${color}`} + /> + ))} +
+
+); diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx new file mode 100644 index 0000000000000..607910f3d4182 --- /dev/null +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import moment, { Moment } from 'moment'; +import React, { FC, useMemo } from 'react'; + +import { LineAnnotation, AnnotationDomainTypes, LineAnnotationStyle } from '@elastic/charts'; +import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; + +interface CurrentTimeProps { + isDarkMode: boolean; + domainEnd?: number | Moment; +} + +/** + * Render current time line annotation on @elastic/charts `Chart` + */ +export const CurrentTime: FC = ({ isDarkMode, domainEnd }) => { + const lineAnnotationStyle = useMemo>( + () => ({ + line: { + strokeWidth: 2, + stroke: isDarkMode ? darkEuiTheme.euiColorDanger : lightEuiTheme.euiColorDanger, + opacity: 0.7, + }, + }), + [isDarkMode] + ); + + // Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if + // the annotation is within this range; if so, the line annotation uses the domainEnd as its value + const now = moment(); + const isAnnotationAtEdge = domainEnd + ? moment(domainEnd).add(1, 'm').isAfter(now) && now.isAfter(domainEnd) + : false; + const lineAnnotationData = [ + { + dataValue: isAnnotationAtEdge ? domainEnd : now.valueOf(), + }, + ]; + + return ( + + ); +}; diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx new file mode 100644 index 0000000000000..b0f9a5c6d7e1b --- /dev/null +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -0,0 +1,174 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { FC, useMemo } from 'react'; +import moment, { unitOfTime } from 'moment'; + +import { TooltipValue, RectAnnotation, RectAnnotationDatum } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import { isUndefined } from 'lodash'; + +interface EndzonesProps { + isDarkMode: boolean; + domainStart: number; + domainEnd: number; + interval: number; + domainMin: number; + domainMax: number; + groupId?: string; +} + +export const Endzones: FC = ({ + isDarkMode, + domainStart, + domainEnd, + interval, + domainMin, + domainMax, + groupId, +}) => { + const rectAnnotationStyle = useMemo( + () => ({ + stroke: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, + strokeWidth: 0, + opacity: isDarkMode ? 0.6 : 0.2, + fill: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, + }), + [isDarkMode] + ); + + const rectAnnotations: RectAnnotationDatum[] = []; + if (domainStart !== domainMin) { + rectAnnotations.push({ + coordinates: { + x1: domainStart, + }, + }); + } + if (domainEnd - interval < domainMax) { + rectAnnotations.push({ + coordinates: { + x0: domainEnd, + }, + details: 'test', + }); + } + + return ( + + ); +}; + +const findIntervalFromDuration = ( + dateValue: number, + esValue: number, + esUnit: unitOfTime.Base, + timeZone: string +) => { + const date = moment.tz(dateValue, timeZone); + const startOfDate = moment.tz(date, timeZone).startOf(esUnit); + const endOfDate = moment.tz(date, timeZone).startOf(esUnit).add(esValue, esUnit); + return endOfDate.valueOf() - startOfDate.valueOf(); +}; + +const getIntervalInMs = ( + value: number, + esValue: number, + esUnit: unitOfTime.Base, + timeZone: string +): number => { + switch (esUnit) { + case 's': + return 1000 * esValue; + case 'ms': + return 1 * esValue; + default: + return findIntervalFromDuration(value, esValue, esUnit, timeZone); + } +}; + +export const getAdjustedInterval = ( + xValues: number[], + esValue: number, + esUnit: string, + timeZone: string +): number => { + return xValues.reduce((minInterval, currentXvalue, index) => { + let currentDiff = minInterval; + if (index > 0) { + currentDiff = Math.abs(xValues[index - 1] - currentXvalue); + } + const singleUnitInterval = getIntervalInMs( + currentXvalue, + esValue, + esUnit as unitOfTime.Base, + timeZone + ); + return Math.min(minInterval, singleUnitInterval, currentDiff); + }, Number.MAX_SAFE_INTEGER); +}; + +export const renderEndzoneTooltip = ( + xInterval?: number, + domainStart?: number, + domainEnd?: number, + formatter?: (v: any) => string +) => (headerData: TooltipValue): JSX.Element | string => { + const headerDataValue = headerData.value; + const formattedValue = formatter ? formatter(headerDataValue) : headerDataValue; + + const partialDataText = i18n.translate('discover.histogram.partialData.bucketTooltipText', { + defaultMessage: + 'The selected time range does not include this entire bucket, it may contain partial data.', + }); + + if ( + (!isUndefined(domainStart) && headerDataValue < domainStart) || + (!isUndefined(domainEnd) && !isUndefined(xInterval) && headerDataValue + xInterval > domainEnd) + ) { + return ( + + + + + + {partialDataText} + + +

{formattedValue}

+
+ ); + } + + return formattedValue; +}; diff --git a/src/plugins/charts/public/static/components/index.ts b/src/plugins/charts/public/static/components/index.ts index 48c9e40145481..c044d361bed18 100644 --- a/src/plugins/charts/public/static/components/index.ts +++ b/src/plugins/charts/public/static/components/index.ts @@ -18,7 +18,7 @@ */ export { BasicOptions } from './basic_options'; -export { ColorModes, Rotates } from './collections'; +export { ColorMode, LabelRotation, defaultCountLabel } from './collections'; export { ColorRanges, SetColorRangeValue } from './color_ranges'; export { ColorSchemaOptions, SetColorSchemaOptionsValue } from './color_schema'; export { ColorSchemaParams, Labels, Style } from './types'; @@ -28,3 +28,7 @@ export { RequiredNumberInputOption } from './required_number_input'; export { SelectOption } from './select'; export { SwitchOption } from './switch'; export { TextInputOption } from './text_input'; +export { LegendToggle } from './legend_toggle'; +export { ColorPicker } from './color_picker'; +export { CurrentTime } from './current_time'; +export * from './endzones'; diff --git a/src/plugins/charts/public/static/components/legend_toggle.tsx b/src/plugins/charts/public/static/components/legend_toggle.tsx new file mode 100644 index 0000000000000..1c6b87853908f --- /dev/null +++ b/src/plugins/charts/public/static/components/legend_toggle.tsx @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo, useMemo } from 'react'; +import classNames from 'classnames'; + +import { i18n } from '@kbn/i18n'; +import { htmlIdGenerator, EuiIcon } from '@elastic/eui'; +import { Position } from '@elastic/charts'; + +import './_legend_toggle.scss'; + +interface LegendToggleProps { + onClick: () => void; + showLegend: boolean; + legendPosition: Position; +} + +const LegendToggleComponent = ({ onClick, showLegend, legendPosition }: LegendToggleProps) => { + const legendId = useMemo(() => htmlIdGenerator()('legend'), []); + + return ( + + ); +}; + +export const LegendToggle = memo(LegendToggleComponent); diff --git a/src/plugins/charts/public/static/components/types.ts b/src/plugins/charts/public/static/components/types.ts index 196eb60b06aec..a4c384141dafb 100644 --- a/src/plugins/charts/public/static/components/types.ts +++ b/src/plugins/charts/public/static/components/types.ts @@ -18,7 +18,7 @@ */ import { ColorSchemas } from '../color_maps'; -import { Rotates } from './collections'; +import { LabelRotation } from './collections'; export interface ColorSchemaParams { colorSchema: ColorSchemas; @@ -29,8 +29,8 @@ export interface Labels { color?: string; filter?: boolean; overwriteColor?: boolean; - rotate?: Rotates; - show: boolean; + rotate?: LabelRotation; + show?: boolean; truncate?: number | null; } diff --git a/src/plugins/charts/public/static/index.ts b/src/plugins/charts/public/static/index.ts index 6fc097d05467f..d8e3f4457689e 100644 --- a/src/plugins/charts/public/static/index.ts +++ b/src/plugins/charts/public/static/index.ts @@ -19,3 +19,4 @@ export * from './color_maps'; export * from './components'; +export * from './utils'; diff --git a/src/plugins/charts/public/static/utils/index.ts b/src/plugins/charts/public/static/utils/index.ts new file mode 100644 index 0000000000000..777deb326125e --- /dev/null +++ b/src/plugins/charts/public/static/utils/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './transform_click_event'; diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts new file mode 100644 index 0000000000000..3675ceb227e2f --- /dev/null +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { XYChartSeriesIdentifier, GeometryValue, XYBrushArea } from '@elastic/charts'; + +import { RangeSelectContext, ValueClickContext } from '../../../../embeddable/public'; +import { KibanaDatatable } from '../../../../expressions/common/expression_types/specs'; + +/** + * Helper function to transform `@elastic/charts` click event into filter action event + */ +export const getFilterFromChartClickEventFn = ( + table: KibanaDatatable, + xAccessor: string | number, + negate: boolean = false +) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ValueClickContext['data'] => { + const data: ValueClickContext['data']['data'] = []; + + points.forEach((point) => { + const [geometry, { yAccessor, splitAccessors }] = point; + const columnIndices = table.columns.reduce((acc, { id }, index) => { + if ([xAccessor, yAccessor, ...splitAccessors.keys()].includes(id)) { + acc.push(index); + } + + return acc; + }, []); + + const rowIndex = table.rows.findIndex((row) => { + return ( + row[xAccessor] === geometry.x && + row[yAccessor] === geometry.y && + [...splitAccessors.entries()].every(([key, value]) => row[key] === value) + ); + }); + + data.push( + ...columnIndices.map((column) => ({ + table, + column, + row: rowIndex, + value: null, + })) + ); + }); + + return { + negate, + data, + }; +}; + +/** + * Helper function to get filter action event from series + */ +export const getFilterFromSeriesFn = (table: KibanaDatatable) => ( + { splitAccessors }: XYChartSeriesIdentifier, + negate = false +): ValueClickContext['data'] => { + const data = table.columns.reduce((acc, { id }, column) => { + if ([...splitAccessors.keys()].includes(id)) { + const value = splitAccessors.get(id); + const row = table.rows.findIndex((r) => r[id] === value); + acc.push({ + table, + column, + row, + value, + }); + } + + return acc; + }, []); + + return { + negate, + data, + }; +}; + +/** + * Helper function to transform `@elastic/charts` brush event into brush action event + */ +export const getBrushFromChartBrushEventFn = ( + table: KibanaDatatable, + xAccessor: string | number +) => ({ x: selectedRange }: XYBrushArea): RangeSelectContext['data'] => { + const [start, end] = selectedRange ?? [0, 0]; + const range: [number, number] = [start, end]; + const column = table.columns.findIndex((c) => c.id === xAccessor); + + return { + table, + column, + range, + }; +}; diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index 4c39c8bb25542..5b609ff7fc204 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -17,25 +17,17 @@ * under the License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import moment from 'moment-timezone'; -import { unitOfTime } from 'moment'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; import { - AnnotationDomainTypes, Axis, Chart, HistogramBarSeries, - LineAnnotation, Position, ScaleType, Settings, - RectAnnotation, - TooltipValue, TooltipType, ElementClickListener, XYChartElementEvent, @@ -43,12 +35,17 @@ import { Theme, } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription, combineLatest } from 'rxjs'; import { getServices } from '../../../kibana_services'; import { Chart as IChart } from '../helpers/point_series'; +import { + CurrentTime, + Endzones, + getAdjustedInterval, + renderEndzoneTooltip, +} from '../../../../../charts/public'; export interface DiscoverHistogramProps { chartData: IChart; @@ -60,34 +57,6 @@ interface DiscoverHistogramState { chartsBaseTheme: Theme; } -function findIntervalFromDuration( - dateValue: number, - esValue: number, - esUnit: unitOfTime.Base, - timeZone: string -) { - const date = moment.tz(dateValue, timeZone); - const startOfDate = moment.tz(date, timeZone).startOf(esUnit); - const endOfDate = moment.tz(date, timeZone).startOf(esUnit).add(esValue, esUnit); - return endOfDate.valueOf() - startOfDate.valueOf(); -} - -function getIntervalInMs( - value: number, - esValue: number, - esUnit: unitOfTime.Base, - timeZone: string -): number { - switch (esUnit) { - case 's': - return 1000 * esValue; - case 'ms': - return 1 * esValue; - default: - return findIntervalFromDuration(value, esValue, esUnit, timeZone); - } -} - function getTimezone(uiSettings: IUiSettingsClient) { if (uiSettings.isDefault('dateFormat:tz')) { const detectedTimezone = moment.tz.guess(); @@ -98,27 +67,6 @@ function getTimezone(uiSettings: IUiSettingsClient) { } } -export function findMinInterval( - xValues: number[], - esValue: number, - esUnit: string, - timeZone: string -): number { - return xValues.reduce((minInterval, currentXvalue, index) => { - let currentDiff = minInterval; - if (index > 0) { - currentDiff = Math.abs(xValues[index - 1] - currentXvalue); - } - const singleUnitInterval = getIntervalInMs( - currentXvalue, - esValue, - esUnit as unitOfTime.Base, - timeZone - ); - return Math.min(minInterval, singleUnitInterval, currentDiff); - }, Number.MAX_SAFE_INTEGER); -} - export class DiscoverHistogram extends Component { public static propTypes = { chartData: PropTypes.object, @@ -132,10 +80,10 @@ export class DiscoverHistogram extends Component + getServices().theme.chartsBaseTheme$, + ]).subscribe(([chartsTheme, chartsBaseTheme]) => this.setState({ chartsTheme, chartsBaseTheme }) ); } @@ -171,40 +119,6 @@ export class DiscoverHistogram extends Component ( - headerData: TooltipValue - ): JSX.Element | string => { - const headerDataValue = headerData.value; - const formattedValue = this.formatXValue(headerDataValue); - - const partialDataText = i18n.translate('discover.histogram.partialData.bucketTooltipText', { - defaultMessage: - 'The selected time range does not include this entire bucket, it may contain partial data.', - }); - - if (headerDataValue < domainStart || headerDataValue + xInterval > domainEnd) { - return ( - - - - - - {partialDataText} - - -

{formattedValue}

-
- ); - } - - return formattedValue; - }; - public render() { const uiSettings = getServices().uiSettings; const timeZone = getTimezone(uiSettings); @@ -216,8 +130,9 @@ export class DiscoverHistogram extends Component domainStart ? domainStart : data[0]?.x; - const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue; + const domainMin = Math.min(data[0]?.x, domainStart); + const domainMax = Math.max(domainEnd - xInterval, lastXValue); const xDomain = { min: domainMin, max: domainMax, - minInterval: findMinInterval(xValues, intervalESValue, intervalESUnit, timeZone), - }; - - // Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if - // the annotation is within this range; if so, the line annotation uses the domainEnd as its value - const now = moment(); - const isAnnotationAtEdge = moment(domainEnd).add(60000).isAfter(now) && now.isAfter(domainEnd); - const lineAnnotationValue = isAnnotationAtEdge ? domainEnd : now; - - const lineAnnotationData = [ - { - dataValue: lineAnnotationValue, - }, - ]; - const isDarkMode = uiSettings.get('theme:darkMode'); - - const lineAnnotationStyle = { - line: { - strokeWidth: 2, - stroke: isDarkMode ? darkEuiTheme.euiColorDanger : lightEuiTheme.euiColorDanger, - opacity: 0.7, - }, + minInterval: getAdjustedInterval(xValues, intervalESValue, intervalESUnit, timeZone), }; - - const rectAnnotations = []; - if (domainStart !== domainMin) { - rectAnnotations.push({ - coordinates: { - x1: domainStart, - }, - }); - } - if (domainEnd !== domainMax) { - rectAnnotations.push({ - coordinates: { - x0: domainEnd, - }, - }); - } - - const rectAnnotationStyle = { - stroke: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, - strokeWidth: 0, - opacity: isDarkMode ? 0.6 : 0.2, - fill: isDarkMode ? darkEuiTheme.euiColorLightShade : lightEuiTheme.euiColorDarkShade, - }; - const tooltipProps = { - headerFormatter: this.renderBarTooltip(xInterval, domainStart, domainEnd), + headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, this.formatXValue), type: TooltipType.VerticalCursor, }; @@ -313,19 +183,14 @@ export class DiscoverHistogram extends Component - - + { +export const toExpressionAst: VisToExpressionAst = (vis) => { const { markdown, fontSize, openLinksInNewTab } = vis.params; const markdownVis = buildExpressionFunction( diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index b58be63581724..2f80809bf0cf2 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -27,14 +27,14 @@ import { Style, } from '../../expressions/public'; import { visType, DimensionsVisParam, VisParams } from './types'; -import { ColorSchemas, vislibColorMaps, ColorModes } from '../../charts/public'; +import { ColorSchemas, vislibColorMaps, ColorMode } from '../../charts/public'; export type Input = KibanaDatatable; interface Arguments { percentageMode: boolean; colorSchema: ColorSchemas; - colorMode: ColorModes; + colorMode: ColorMode; useRanges: boolean; invertColors: boolean; showLabels: boolean; @@ -86,7 +86,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ colorMode: { types: ['string'], default: '"None"', - options: [ColorModes.NONE, ColorModes.LABELS, ColorModes.BACKGROUND], + options: [ColorMode.None, ColorMode.Labels, ColorMode.Background], help: i18n.translate('visTypeMetric.function.colorMode.help', { defaultMessage: 'Which part of metric to color', }), @@ -194,8 +194,8 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ invertColors: args.invertColors, style: { bgFill: args.bgFill, - bgColor: args.colorMode === ColorModes.BACKGROUND, - labelColor: args.colorMode === ColorModes.LABELS, + bgColor: args.colorMode === ColorMode.Background, + labelColor: args.colorMode === ColorMode.Labels, subText: args.subText, fontSize, }, diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index 6b4d6e151693f..6e026c987606c 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { MetricVisOptions } from './components/metric_vis_options'; -import { ColorSchemas, colorSchemas, ColorModes } from '../../charts/public'; +import { ColorSchemas, colorSchemas, ColorMode } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { toExpressionAst } from './to_ast'; @@ -41,7 +41,7 @@ export const createMetricVisTypeDefinition = () => ({ percentageMode: false, useRanges: false, colorSchema: ColorSchemas.GreenToRed, - metricColorMode: ColorModes.NONE, + metricColorMode: ColorMode.None, colorsRange: [{ from: 0, to: 10000 }], labels: { show: true, @@ -61,19 +61,19 @@ export const createMetricVisTypeDefinition = () => ({ collections: { metricColorMode: [ { - id: ColorModes.NONE, + id: ColorMode.None, label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { defaultMessage: 'None', }), }, { - id: ColorModes.LABELS, + id: ColorMode.Labels, label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { defaultMessage: 'Labels', }), }, { - id: ColorModes.BACKGROUND, + id: ColorMode.Background, label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { defaultMessage: 'Background', }), diff --git a/src/plugins/vis_type_vislib/kibana.json b/src/plugins/vis_type_vislib/kibana.json index 720abff16b7c7..121ab6b080170 100644 --- a/src/plugins/vis_type_vislib/kibana.json +++ b/src/plugins/vis_type_vislib/kibana.json @@ -5,5 +5,5 @@ "ui": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"], "optionalPlugins": ["visTypeXy"], - "requiredBundles": ["kibanaUtils", "visDefaultEditor"] + "requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy"] } diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts index ec90fbd1746a1..fea3615922133 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -17,177 +17,18 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - InterpolationModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ - name: 'area', - title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }), - icon: 'visArea', - description: i18n.translate('visTypeVislib.area.areaDescription', { - defaultMessage: 'Emphasize the quantity beneath a line chart', - }), + ...xyVisTypes.area, + toExpression, visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, visConfig: { - defaults: { - type: 'area', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.AREA, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1', - }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: InterpolationModes.LINEAR, - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - labels: {}, - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.area.metricsTitle', { - defaultMessage: 'Y-axis', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - min: 1, - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.area.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.area.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.area.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.area.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), + ...xyVisTypes.area.visConfig, + component: undefined, }, }); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx deleted file mode 100644 index 524792d1460fe..0000000000000 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { mount, shallow } from 'enzyme'; - -import { IAggConfig, IAggType } from 'src/plugins/data/public'; -import { MetricsAxisOptions } from './index'; -import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; -import { ValidationVisOptionsProps } from '../../common'; -import { Positions } from '../../../utils/collections'; -import { ValueAxesPanel } from './value_axes_panel'; -import { CategoryAxisPanel } from './category_axis_panel'; -import { ChartTypes } from '../../../utils/collections'; -import { defaultValueAxisId, valueAxis, seriesParam, categoryAxis } from './mocks'; - -jest.mock('./series_panel', () => ({ - SeriesPanel: () => 'SeriesPanel', -})); -jest.mock('./category_axis_panel', () => ({ - CategoryAxisPanel: () => 'CategoryAxisPanel', -})); -jest.mock('./value_axes_panel', () => ({ - ValueAxesPanel: () => 'ValueAxesPanel', -})); - -const SERIES_PARAMS = 'seriesParams'; -const VALUE_AXES = 'valueAxes'; - -const aggCount: IAggConfig = { - id: '1', - type: { name: 'count' }, - makeLabel: () => 'Count', -} as IAggConfig; - -const aggAverage: IAggConfig = { - id: '2', - type: { name: 'average' } as IAggType, - makeLabel: () => 'Average', -} as IAggConfig; - -const createAggs = (aggs: any[]) => ({ - aggs, - bySchemaName: () => aggs, -}); - -describe('MetricsAxisOptions component', () => { - let setValue: jest.Mock; - let defaultProps: ValidationVisOptionsProps; - let axis: ValueAxis; - let axisRight: ValueAxis; - let chart: SeriesParam; - - beforeEach(() => { - setValue = jest.fn(); - - axis = { - ...valueAxis, - name: 'LeftAxis-1', - position: Positions.LEFT, - }; - axisRight = { - ...valueAxis, - id: 'ValueAxis-2', - name: 'RightAxis-1', - position: Positions.RIGHT, - }; - chart = { - ...seriesParam, - type: ChartTypes.AREA, - }; - - defaultProps = { - aggs: createAggs([aggCount]), - isTabSelected: true, - vis: { - type: { - type: ChartTypes.AREA, - schemas: { metrics: [{ name: 'metric' }] }, - }, - setState: jest.fn(), - serialize: jest.fn(), - }, - stateParams: { - valueAxes: [axis], - seriesParams: [chart], - categoryAxes: [categoryAxis], - grid: { valueAxis: defaultValueAxisId }, - }, - setValue, - } as any; - }); - - it('should init with the default set of props', () => { - const comp = shallow(); - - expect(comp).toMatchSnapshot(); - }); - - describe('useEffect', () => { - it('should update series when new agg is added', () => { - const comp = mount(); - comp.setProps({ - aggs: createAggs([aggCount, aggAverage]), - }); - - const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; - expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); - }); - - it('should update series when new agg label is changed', () => { - const comp = mount(); - const agg = { id: aggCount.id, makeLabel: () => 'New label' }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; - expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); - }); - - it('should update visType when one seriesParam', () => { - const comp = mount(); - expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); - - comp.setProps({ - stateParams: { - ...defaultProps.stateParams, - seriesParams: [{ ...chart, type: ChartTypes.LINE }], - }, - }); - - expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.LINE }); - }); - - it('should set histogram visType when multiple seriesParam', () => { - const comp = mount(); - expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); - - comp.setProps({ - stateParams: { - ...defaultProps.stateParams, - seriesParams: [chart, { ...chart, type: ChartTypes.LINE }], - }, - }); - - expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.HISTOGRAM }); - }); - }); - - describe('updateAxisTitle', () => { - it('should not update the value axis title if custom title was set', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const comp = mount(); - const newAgg = { - ...aggCount, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([newAgg]), - }); - const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); - - it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { - const comp = mount(); - const agg = { - id: aggCount.id, - params: { customLabel: 'Custom label' }, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - const updatedSeriesParams = [{ ...chart, data: { ...chart.data, label: agg.makeLabel() } }]; - const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; - - expect(setValue).toHaveBeenCalledTimes(5); - expect(setValue).toHaveBeenNthCalledWith(3, SERIES_PARAMS, updatedSeriesParams); - expect(setValue).toHaveBeenNthCalledWith(5, SERIES_PARAMS, updatedSeriesParams); - expect(setValue).toHaveBeenNthCalledWith(4, VALUE_AXES, updatedValues); - }); - - it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { - const comp = mount(); - const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; - comp.setProps({ - aggs: createAggs([agg, aggAverage]), - stateParams: { - ...defaultProps.stateParams, - seriesParams: [chart, chart], - }, - }); - - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); - }); - - it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const comp = mount(); - const agg = { - id: aggCount.id, - params: { customLabel: 'Custom label' }, - makeLabel: () => 'Custom label', - }; - comp.setProps({ - aggs: createAggs([agg]), - }); - - expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); - }); - }); - - it('should add value axis', () => { - const comp = shallow(); - comp.find(ValueAxesPanel).prop('addValueAxis')(); - - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); - }); - - describe('removeValueAxis', () => { - beforeEach(() => { - defaultProps.stateParams.valueAxes = [axis, axisRight]; - }); - - it('should remove value axis', () => { - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); - }); - - it('should update seriesParams "valueAxis" prop', () => { - const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); - }); - - it('should reset grid "valueAxis" prop', () => { - const updatedGrid = { valueAxis: undefined }; - defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; - const comp = shallow(); - comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); - - expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); - }); - }); - - it('should update axis value when when category position chnaged', () => { - const comp = shallow(); - comp.find(CategoryAxisPanel).prop('onPositionChanged')(Positions.LEFT); - - const updatedValues = [{ ...axis, name: 'BottomAxis-1', position: Positions.BOTTOM }]; - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); -}); diff --git a/src/plugins/vis_type_vislib/public/editor/collections.ts b/src/plugins/vis_type_vislib/public/editor/collections.ts new file mode 100644 index 0000000000000..f1caa0754b0b3 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/editor/collections.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +import { colorSchemas } from '../../../charts/public'; +import { getPositions, getScaleTypes } from '../../../vis_type_xy/public'; + +import { Alignment, GaugeType } from '../types'; + +export const getGaugeTypes = () => [ + { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { + defaultMessage: 'Arc', + }), + value: GaugeType.Arc, + }, + { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { + defaultMessage: 'Circle', + }), + value: GaugeType.Circle, + }, +]; + +export const getAlignments = () => [ + { + text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { + defaultMessage: 'Automatic', + }), + value: Alignment.Automatic, + }, + { + text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { + defaultMessage: 'Horizontal', + }), + value: Alignment.Horizontal, + }, + { + text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { + defaultMessage: 'Vertical', + }), + value: Alignment.Vertical, + }, +]; + +export const getGaugeCollections = () => ({ + gaugeTypes: getGaugeTypes(), + alignments: getAlignments(), + colorSchemas, +}); + +export const getHeatmapCollections = () => ({ + legendPositions: getPositions(), + scales: getScaleTypes(), + colorSchemas, +}); diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/index.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/index.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/labels_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/ranges_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/gauge/style_panel.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx similarity index 97% rename from src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx rename to src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx index 7a89496d9441e..067d2c62eae46 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/index.tsx @@ -23,7 +23,8 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { VisOptionsProps } from '../../../../../vis_default_editor/public'; +import { ValueAxis } from '../../../../../vis_type_xy/public'; import { BasicOptions, ColorRanges, @@ -34,8 +35,8 @@ import { SetColorSchemaOptionsValue, SetColorRangeValue, } from '../../../../../charts/public'; + import { HeatmapVisParams } from '../../../heatmap'; -import { ValueAxis } from '../../../types'; import { LabelsPanel } from './labels_panel'; function HeatmapOptions(props: VisOptionsProps) { diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx rename to src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx index 4998a8fd02521..8ec06ea50ec12 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/heatmap/labels_panel.tsx @@ -23,10 +23,11 @@ import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValueAxis } from '../../../types'; -import { HeatmapVisParams } from '../../../heatmap'; +import { VisOptionsProps } from '../../../../../vis_default_editor/public'; import { SwitchOption } from '../../../../../charts/public'; +import { ValueAxis } from '../../../../../vis_type_xy/public'; + +import { HeatmapVisParams } from '../../../heatmap'; const VERTICAL_ROTATION = 270; diff --git a/src/plugins/vis_type_vislib/public/editor/components/index.ts b/src/plugins/vis_type_vislib/public/editor/components/index.ts new file mode 100644 index 0000000000000..aff9ac523c846 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/editor/components/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './gauge'; +export * from './heatmap'; +export * from './pie'; diff --git a/src/plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx similarity index 83% rename from src/plugins/vis_type_vislib/public/components/options/pie.tsx rename to src/plugins/vis_type_vislib/public/editor/components/pie.tsx index 54ba307982967..ebf35e0a70bb5 100644 --- a/src/plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx @@ -22,9 +22,10 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { TruncateLabelsOption } from '../common'; +import { VisOptionsProps } from '../../../../vis_default_editor/public'; import { BasicOptions, SwitchOption } from '../../../../charts/public'; +import { TruncateLabelsOption } from '../../../../vis_type_xy/public'; + import { PieVisParams } from '../../pie'; function PieOptions(props: VisOptionsProps) { @@ -40,14 +41,14 @@ function PieOptions(props: VisOptionsProps) {

) {

) { setValue={setLabels} /> ) { setValue={setLabels} /> defaultMessage: "Gauges indicate the status of a metric. Use it to show how a metric's value relates to reference threshold values.", }), + toExpression, + visualization: createVislibVisController(deps), visConfig: { defaults: { type: 'gauge', @@ -70,15 +74,15 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => addLegend: true, isDisplayWarning: false, gauge: { - alignment: Alignments.AUTOMATIC, + alignment: Alignment.Automatic, extendRange: true, percentageMode: false, - gaugeType: GaugeTypes.ARC, + gaugeType: GaugeType.Arc, gaugeStyle: 'Full', backStyle: 'Full', orientation: 'vertical', colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.LABELS, + gaugeColorMode: ColorMode.Labels, colorsRange: [ { from: 0, to: 50 }, { from: 50, to: 75 }, @@ -109,7 +113,6 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, }, }, - visualization: createVislibVisController(deps), editorConfig: { collections: getGaugeCollections(), optionsTemplate: GaugeOptions, diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 5f74698938a0b..b1d2f6ac6e6d5 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -19,13 +19,15 @@ import { i18n } from '@kbn/i18n'; -import { GaugeOptions } from './components/options'; -import { getGaugeCollections, GaugeTypes } from './utils/collections'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; -import { ColorModes, ColorSchemas } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; +import { ColorMode, ColorSchemas } from '../../charts/public'; + +import { getGaugeCollections, GaugeOptions } from './editor'; +import { createVislibVisController } from './vis_controller'; +import { VisTypeVislibDependencies } from './plugin'; +import { toExpression } from './to_expression'; +import { GaugeType } from './types'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', @@ -34,6 +36,7 @@ export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => description: i18n.translate('visTypeVislib.goal.goalDescription', { defaultMessage: 'A goal chart indicates how close you are to your final goal.', }), + toExpression, visualization: createVislibVisController(deps), visConfig: { defaults: { @@ -45,13 +48,13 @@ export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => verticalSplit: false, autoExtend: false, percentageMode: true, - gaugeType: GaugeTypes.ARC, + gaugeType: GaugeType.Arc, gaugeStyle: 'Full', backStyle: 'Full', orientation: 'vertical', useRanges: false, colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.NONE, + gaugeColorMode: ColorMode.None, colorsRange: [{ from: 0, to: 10000 }], invertColors: false, labels: { diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index bd3d02029cb23..93d2419eda944 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -19,16 +19,19 @@ import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; import { RangeValues, Schemas } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; -import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; -import { HeatmapOptions } from './components/options'; +import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; +import { ValueAxis, ScaleType, AxisType } from '../../vis_type_xy/public'; + +import { HeatmapOptions, getHeatmapCollections } from './editor'; import { createVislibVisController } from './vis_controller'; import { TimeMarker } from './vislib/visualizations/time_marker'; -import { CommonVislibParams, ValueAxis } from './types'; +import { CommonVislibParams } from './types'; import { VisTypeVislibDependencies } from './plugin'; -import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -49,6 +52,7 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), + toExpression, getSupportedTriggers: () => { return [VIS_EVENT_TO_TRIGGER.filter]; }, @@ -59,7 +63,7 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) addTooltip: true, addLegend: true, enableHover: false, - legendPosition: Positions.RIGHT, + legendPosition: Position.Right, times: [], colorsNumber: 4, colorSchema: ColorSchemas.Greens, @@ -71,9 +75,9 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) { show: false, id: 'ValueAxis-1', - type: AxisTypes.VALUE, + type: AxisType.Value, scale: { - type: ScaleTypes.LINEAR, + type: ScaleType.Linear, defaultYExtents: false, }, labels: { diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 8aeeb4ec533ab..bad0531badc9f 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -17,179 +17,18 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ - name: 'histogram', - title: i18n.translate('visTypeVislib.histogram.histogramTitle', { - defaultMessage: 'Vertical Bar', - }), - icon: 'visBarVertical', - description: i18n.translate('visTypeVislib.histogram.histogramDescription', { - defaultMessage: 'Assign a continuous variable to each axis', - }), + ...xyVisTypes.histogram, + toExpression, visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: { - show: false, - }, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.histogram.metricTitle', { - defaultMessage: 'Y-axis', - }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.histogram.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.histogram.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.histogram.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.histogram.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), + ...xyVisTypes.histogram.visConfig, + component: undefined, }, }); diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index 702581828e60d..4713124da4af2 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -17,176 +17,18 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ - name: 'horizontal_bar', - title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', { - defaultMessage: 'Horizontal Bar', - }), - icon: 'visBarHorizontal', - description: i18n.translate('visTypeVislib.horizontalBar.horizontalBarDescription', { - defaultMessage: 'Assign a continuous variable to each axis', - }), + ...xyVisTypes.horizontalBar, + toExpression, visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 200, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.ANGLED, - filter: true, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.horizontalBar.metricTitle', { - defaultMessage: 'Y-axis', - }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.horizontalBar.radiusTitle', { - defaultMessage: 'Dot size', - }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.horizontalBar.segmentTitle', { - defaultMessage: 'X-axis', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.horizontalBar.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.horizontalBar.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), + ...xyVisTypes.horizontalBar.visConfig, + component: undefined, }, }); diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index 6e9190229114b..540e64f9661a9 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -17,169 +17,18 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { xyVisTypes } from '../../vis_type_xy/public'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { - Positions, - ChartTypes, - ChartModes, - AxisTypes, - ScaleTypes, - AxisModes, - ThresholdLineStyles, - InterpolationModes, - getConfigCollections, -} from './utils/collections'; -import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ - name: 'line', - title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }), - icon: 'visLine', - description: i18n.translate('visTypeVislib.line.lineDescription', { - defaultMessage: 'Emphasize trends', - }), + ...xyVisTypes.line, + toExpression, visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, visConfig: { - defaults: { - type: 'line', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100, - }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100, - }, - title: { - text: countLabel, - }, - }, - ], - seriesParams: [ - { - show: true, - type: ChartTypes.LINE, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1', - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - interpolate: InterpolationModes.LINEAR, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: euiPaletteColorBlind()[9], - }, - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeVislib.line.metricTitle', { defaultMessage: 'Y-axis' }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('visTypeVislib.line.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('visTypeVislib.line.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeVislib.line.groupTitle', { - defaultMessage: 'Split series', - }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeVislib.line.splitTitle', { - defaultMessage: 'Split chart', - }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - }, - ]), + ...xyVisTypes.line.visConfig, + component: undefined, }, }); diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index 1e81dbdde3f68..adf429e484b17 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -18,15 +18,18 @@ */ import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; -import { PieOptions } from './components/options'; -import { getPositions, Positions } from './utils/collections'; +import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; +import { getPositions } from '../../vis_type_xy/public'; + import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; import { VisTypeVislibDependencies } from './plugin'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpression } from './to_expression'; +import { PieOptions } from './editor'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; @@ -47,6 +50,7 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( description: i18n.translate('visTypeVislib.pie.pieDescription', { defaultMessage: 'Compare parts of a whole', }), + toExpression, visualization: createVislibVisController(deps), getSupportedTriggers: () => { return [VIS_EVENT_TO_TRIGGER.filter]; @@ -56,7 +60,7 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( type: 'pie', addTooltip: true, addLegend: true, - legendPosition: Positions.RIGHT, + legendPosition: Position.Right, isDonut: true, labels: { show: false, diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index c6a6b6f82592b..5912425af3be3 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -80,35 +80,30 @@ export class VisTypeVislibPlugin implements Plugin { uiSettings: core.uiSettings, charts, }; - const vislibTypes = [ - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, - ]; - const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy) { - const convertedTypes: any[] = []; - const convertedFns: any[] = []; - - // Register legacy vislib types that have been converted - convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + if (visTypeXy) { + [ + createPieVisTypeDefinition, + createHeatmapVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, + ].forEach((vis) => visualizations.createBaseVisualization(vis(visualizationDependencies))); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); + } else { + // Register all vis types + [ + createHistogramVisTypeDefinition, + createLineVisTypeDefinition, + createPieVisTypeDefinition, + createAreaVisTypeDefinition, + createHeatmapVisTypeDefinition, + createHorizontalBarVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, + ].forEach((vis) => visualizations.createBaseVisualization(vis(visualizationDependencies))); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); } - - // Register non-converted types - vislibFns.forEach(expressions.registerFunction); - vislibTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); } public start(core: CoreStart, { data, kibanaLegacy }: VisTypeVislibPluginStartDependencies) { diff --git a/src/plugins/vis_type_vislib/public/to_expression.ts b/src/plugins/vis_type_vislib/public/to_expression.ts new file mode 100644 index 0000000000000..2506d0aea7091 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_expression.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { get } from 'lodash'; + +import { SchemaConfig, ExpressionFn } from '../../visualizations/public'; + +export type XSchemaConfig = Omit & { + params: { + date?: boolean; + interval?: number; + format?: any; + bounds?: any; + }; +}; + +export interface Dimensions { + x: XSchemaConfig | null; + y: SchemaConfig[]; + z?: SchemaConfig[]; + width?: SchemaConfig[]; + series?: SchemaConfig[]; + splitRow?: SchemaConfig[]; + splitColumn?: SchemaConfig[]; +} + +export const toExpression: ExpressionFn = async (vis, params, schemas): Promise => { + 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().filter(({ enabled }) => enabled) ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === 'date_histogram') { + dimensions.x.params.date = true; + const { esUnit, esValue } = xAgg.buckets.getInterval(); + dimensions.x.params.interval = moment.duration(esValue, esUnit).asMilliseconds(); + dimensions.x.params.format = xAgg.buckets.getScaledDateFormat(); + dimensions.x.params.bounds = xAgg.buckets.getBounds(); + } else if (xAgg.type.name === '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.interval = output.params.interval; + } + } + + const visConfig = vis.params; + + (dimensions.y || []).forEach((yDimension) => { + const yAgg = responseAggs[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find( + (param: any) => param.data.id === yAgg.id + ); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis: any) => valueAxis.id === seriesParam.valueAxis + ); + if (get(usedValueAxis, 'scale.mode') === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + if (get(visConfig, 'gauge.percentageMode') === true) { + yDimension.format = { id: 'percent' }; + } + }); + + visConfig.dimensions = dimensions; + + const configStr = `visConfig='${JSON.stringify(visConfig) + .replace(/\\/g, `\\\\`) + .replace(/'/g, `\\'`)}' `; + return `vislib type='${vis.type.name}' ${configStr}`; +}; diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index 83d0b49b1c551..ef2701a542394 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -17,80 +17,55 @@ * under the License. */ -import { TimeMarker } from './vislib/visualizations/time_marker'; -import { - Positions, - ChartModes, - ChartTypes, - AxisModes, - AxisTypes, - InterpolationModes, - ScaleTypes, - ThresholdLineStyles, -} from './utils/collections'; -import { Labels, Style } from '../../charts/public'; - -export interface CommonVislibParams { - addTooltip: boolean; - legendPosition: Positions; -} +import { $Values } from '@kbn/utility-types'; +import { Position } from '@elastic/charts'; -export interface Scale { - boundsMargin?: number | ''; - defaultYExtents?: boolean; - max?: number | null; - min?: number | null; - mode?: AxisModes; - setYExtents?: boolean; - type: ScaleTypes; -} +import { Labels } from '../../charts/public'; +import { ChartType } from '../../vis_type_xy/common/types'; +import { + CategoryAxis, + Dimensions, + Grid, + SeriesParam, + ThresholdLine, + ValueAxis, +} from '../../vis_type_xy/public'; -interface ThresholdLine { - show: boolean; - value: number | null; - width: number | null; - style: ThresholdLineStyles; - color: string; -} +import { TimeMarker } from './vislib/visualizations/time_marker'; -export interface Axis { - id: string; - labels: Labels; - position: Positions; - scale: Scale; - show: boolean; - style: Style; - title: { text: string }; - type: AxisTypes; -} +/** + * Gauge title alignment + */ +export const Alignment = Object.freeze({ + Automatic: 'automatic' as 'automatic', + Horizontal: 'horizontal' as 'horizontal', + Vertical: 'vertical' as 'vertical', +}); +export type Alignment = $Values; -export interface ValueAxis extends Axis { - name: string; -} +export const GaugeType = Object.freeze({ + Arc: 'Arc' as 'Arc', + Circle: 'Circle' as 'Circle', +}); +export type GaugeType = $Values; -export interface SeriesParam { - data: { label: string; id: string }; - drawLinesBetweenPoints: boolean; - interpolate: InterpolationModes; - lineWidth?: number; - mode: ChartModes; - show: boolean; - showCircles: boolean; - type: ChartTypes; - valueAxis: string; +export interface CommonVislibParams { + addTooltip: boolean; + legendPosition: Position; } export interface BasicVislibParams extends CommonVislibParams { + type: ChartType; + addLegend: boolean; addTimeMarker: boolean; - categoryAxes: Axis[]; + categoryAxes: CategoryAxis[]; orderBucketsBySum?: boolean; labels: Labels; thresholdLine: ThresholdLine; valueAxes: ValueAxis[]; - grid: { - categoryLines: boolean; - valueAxis?: string; - }; + grid: Grid; seriesParams: SeriesParam[]; times: TimeMarker[]; + dimensions: Dimensions; + radiusRatio: number; } diff --git a/src/plugins/vis_type_vislib/public/utils/collections.ts b/src/plugins/vis_type_vislib/public/utils/collections.ts deleted file mode 100644 index 44df4864bfd68..0000000000000 --- a/src/plugins/vis_type_vislib/public/utils/collections.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; -import { $Values } from '@kbn/utility-types'; - -import { colorSchemas, Rotates } from '../../../charts/public'; - -export const Positions = Object.freeze({ - RIGHT: 'right' as 'right', - LEFT: 'left' as 'left', - TOP: 'top' as 'top', - BOTTOM: 'bottom' as 'bottom', -}); -export type Positions = $Values; - -const getPositions = () => [ - { - text: i18n.translate('visTypeVislib.legendPositions.topText', { - defaultMessage: 'Top', - }), - value: Positions.TOP, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.leftText', { - defaultMessage: 'Left', - }), - value: Positions.LEFT, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.rightText', { - defaultMessage: 'Right', - }), - value: Positions.RIGHT, - }, - { - text: i18n.translate('visTypeVislib.legendPositions.bottomText', { - defaultMessage: 'Bottom', - }), - value: Positions.BOTTOM, - }, -]; - -export const ChartTypes = Object.freeze({ - LINE: 'line' as 'line', - AREA: 'area' as 'area', - HISTOGRAM: 'histogram' as 'histogram', -}); -export type ChartTypes = $Values; - -const getChartTypes = () => [ - { - text: i18n.translate('visTypeVislib.chartTypes.lineText', { - defaultMessage: 'Line', - }), - value: ChartTypes.LINE, - }, - { - text: i18n.translate('visTypeVislib.chartTypes.areaText', { - defaultMessage: 'Area', - }), - value: ChartTypes.AREA, - }, - { - text: i18n.translate('visTypeVislib.chartTypes.barText', { - defaultMessage: 'Bar', - }), - value: ChartTypes.HISTOGRAM, - }, -]; - -export const ChartModes = Object.freeze({ - NORMAL: 'normal' as 'normal', - STACKED: 'stacked' as 'stacked', -}); -export type ChartModes = $Values; - -const getChartModes = () => [ - { - text: i18n.translate('visTypeVislib.chartModes.normalText', { - defaultMessage: 'Normal', - }), - value: ChartModes.NORMAL, - }, - { - text: i18n.translate('visTypeVislib.chartModes.stackedText', { - defaultMessage: 'Stacked', - }), - value: ChartModes.STACKED, - }, -]; - -export const InterpolationModes = Object.freeze({ - LINEAR: 'linear' as 'linear', - CARDINAL: 'cardinal' as 'cardinal', - STEP_AFTER: 'step-after' as 'step-after', -}); -export type InterpolationModes = $Values; - -const getInterpolationModes = () => [ - { - text: i18n.translate('visTypeVislib.interpolationModes.straightText', { - defaultMessage: 'Straight', - }), - value: InterpolationModes.LINEAR, - }, - { - text: i18n.translate('visTypeVislib.interpolationModes.smoothedText', { - defaultMessage: 'Smoothed', - }), - value: InterpolationModes.CARDINAL, - }, - { - text: i18n.translate('visTypeVislib.interpolationModes.steppedText', { - defaultMessage: 'Stepped', - }), - value: InterpolationModes.STEP_AFTER, - }, -]; - -export const AxisTypes = Object.freeze({ - CATEGORY: 'category' as 'category', - VALUE: 'value' as 'value', -}); -export type AxisTypes = $Values; - -export const ScaleTypes = Object.freeze({ - LINEAR: 'linear' as 'linear', - LOG: 'log' as 'log', - SQUARE_ROOT: 'square root' as 'square root', -}); -export type ScaleTypes = $Values; - -const getScaleTypes = () => [ - { - text: i18n.translate('visTypeVislib.scaleTypes.linearText', { - defaultMessage: 'Linear', - }), - value: ScaleTypes.LINEAR, - }, - { - text: i18n.translate('visTypeVislib.scaleTypes.logText', { - defaultMessage: 'Log', - }), - value: ScaleTypes.LOG, - }, - { - text: i18n.translate('visTypeVislib.scaleTypes.squareRootText', { - defaultMessage: 'Square root', - }), - value: ScaleTypes.SQUARE_ROOT, - }, -]; - -export const AxisModes = Object.freeze({ - NORMAL: 'normal' as 'normal', - PERCENTAGE: 'percentage' as 'percentage', - WIGGLE: 'wiggle' as 'wiggle', - SILHOUETTE: 'silhouette' as 'silhouette', -}); -export type AxisModes = $Values; - -const getAxisModes = () => [ - { - text: i18n.translate('visTypeVislib.axisModes.normalText', { - defaultMessage: 'Normal', - }), - value: AxisModes.NORMAL, - }, - { - text: i18n.translate('visTypeVislib.axisModes.percentageText', { - defaultMessage: 'Percentage', - }), - value: AxisModes.PERCENTAGE, - }, - { - text: i18n.translate('visTypeVislib.axisModes.wiggleText', { - defaultMessage: 'Wiggle', - }), - value: AxisModes.WIGGLE, - }, - { - text: i18n.translate('visTypeVislib.axisModes.silhouetteText', { - defaultMessage: 'Silhouette', - }), - value: AxisModes.SILHOUETTE, - }, -]; - -export const ThresholdLineStyles = Object.freeze({ - FULL: 'full' as 'full', - DASHED: 'dashed' as 'dashed', - DOT_DASHED: 'dot-dashed' as 'dot-dashed', -}); -export type ThresholdLineStyles = $Values; - -const getThresholdLineStyles = () => [ - { - value: ThresholdLineStyles.FULL, - text: i18n.translate('visTypeVislib.thresholdLine.style.fullText', { - defaultMessage: 'Full', - }), - }, - { - value: ThresholdLineStyles.DASHED, - text: i18n.translate('visTypeVislib.thresholdLine.style.dashedText', { - defaultMessage: 'Dashed', - }), - }, - { - value: ThresholdLineStyles.DOT_DASHED, - text: i18n.translate('visTypeVislib.thresholdLine.style.dotdashedText', { - defaultMessage: 'Dot-dashed', - }), - }, -]; - -const getRotateOptions = () => [ - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.horizontalText', { - defaultMessage: 'Horizontal', - }), - value: Rotates.HORIZONTAL, - }, - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.verticalText', { - defaultMessage: 'Vertical', - }), - value: Rotates.VERTICAL, - }, - { - text: i18n.translate('visTypeVislib.categoryAxis.rotate.angledText', { - defaultMessage: 'Angled', - }), - value: Rotates.ANGLED, - }, -]; - -export const GaugeTypes = Object.freeze({ - ARC: 'Arc' as 'Arc', - CIRCLE: 'Circle' as 'Circle', -}); -export type GaugeTypes = $Values; - -const getGaugeTypes = () => [ - { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { - defaultMessage: 'Arc', - }), - value: GaugeTypes.ARC, - }, - { - text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { - defaultMessage: 'Circle', - }), - value: GaugeTypes.CIRCLE, - }, -]; - -export const Alignments = Object.freeze({ - AUTOMATIC: 'automatic' as 'automatic', - HORIZONTAL: 'horizontal' as 'horizontal', - VERTICAL: 'vertical' as 'vertical', -}); -export type Alignments = $Values; - -const getAlignments = () => [ - { - text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { - defaultMessage: 'Automatic', - }), - value: Alignments.AUTOMATIC, - }, - { - text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { - defaultMessage: 'Horizontal', - }), - value: Alignments.HORIZONTAL, - }, - { - text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { - defaultMessage: 'Vertical', - }), - value: Alignments.VERTICAL, - }, -]; - -const getConfigCollections = () => ({ - legendPositions: getPositions(), - positions: getPositions(), - chartTypes: getChartTypes(), - axisModes: getAxisModes(), - scaleTypes: getScaleTypes(), - chartModes: getChartModes(), - interpolationModes: getInterpolationModes(), - thresholdLineStyles: getThresholdLineStyles(), -}); - -const getGaugeCollections = () => ({ - gaugeTypes: getGaugeTypes(), - alignments: getAlignments(), - colorSchemas, -}); - -const getHeatmapCollections = () => ({ - legendPositions: getPositions(), - scales: getScaleTypes(), - colorSchemas, -}); - -export { - getConfigCollections, - getGaugeCollections, - getHeatmapCollections, - getPositions, - getRotateOptions, - getScaleTypes, - getInterpolationModes, - getChartTypes, - getChartModes, - getAxisModes, -}; diff --git a/src/plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx index 86ef98de045d7..51621e26e311d 100644 --- a/src/plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -20,13 +20,15 @@ import $ from 'jquery'; import React, { RefObject } from 'react'; +import { Position } from '@elastic/charts'; + +import { mountReactNode } from '../../../core/public/utils'; +import { VisParams, ExprVis } from '../../visualizations/public'; + // @ts-ignore import { Vis as Vislib } from './vislib/vis'; -import { Positions } from './utils/collections'; import { VisTypeVislibDependencies } from './plugin'; -import { mountReactNode } from '../../../core/public/utils'; import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; -import { VisParams, ExprVis } from '../../visualizations/public'; import { getKibanaLegacy } from './services'; const legendClassName = { @@ -113,7 +115,7 @@ export const createVislibVisController = (deps: VisTypeVislibDependencies) => { }); } - mountLegend(visData: any, position: Positions) { + mountLegend(visData: any, position: Position) { this.unmount = mountReactNode( > => ({ - name: 'vislib', + name, type: 'render', inputTypes: ['kibana_datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { diff --git a/src/plugins/vis_type_xy/README.md b/src/plugins/vis_type_xy/README.md index 70ddb21c1e9db..039ec2396e099 100644 --- a/src/plugins/vis_type_xy/README.md +++ b/src/plugins/vis_type_xy/README.md @@ -1,2 +1,2 @@ Contains the new xy-axis chart using the elastic-charts library, which will eventually -replace the vislib xy-axis (bar, area, line) charts. \ No newline at end of file +replace the vislib xy-axis charts including bar, area and line. diff --git a/src/plugins/vis_type_xy/common/types.ts b/src/plugins/vis_type_xy/common/types.ts new file mode 100644 index 0000000000000..c0b4abf665167 --- /dev/null +++ b/src/plugins/vis_type_xy/common/types.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { $Values } from '@kbn/utility-types'; + +export const ChartType = Object.freeze({ + Line: 'line' as const, + Area: 'area' as const, + Histogram: 'histogram' as const, +}); +export type ChartType = $Values; diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index ca02da45e9112..14c3ce36bf375 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["charts", "expressions", "visualizations"] + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "requiredBundles": ["kibanaUtils", "visDefaultEditor"] } diff --git a/src/plugins/vis_type_xy/public/_chart.scss b/src/plugins/vis_type_xy/public/_chart.scss new file mode 100644 index 0000000000000..1f49892b870f4 --- /dev/null +++ b/src/plugins/vis_type_xy/public/_chart.scss @@ -0,0 +1,5 @@ +.xyChart__container { + width: 100%; + height: 100%; + position: relative; +} diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts new file mode 100644 index 0000000000000..d8d55c77b7a8a --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { XYAxis } from './xy_axis'; +export { XYEndzones } from './xy_endzones'; +export { XYCurrentTime } from './xy_current_time'; +export { XYSettings } from './xy_settings'; +export { XYThresholdLine } from './xy_threshold_line'; +export { SplitChartWarning } from './split_chart_warning'; diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx new file mode 100644 index 0000000000000..0f618cc89f615 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageContentBody, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const SplitChartWarning: FC = () => { + return ( + + + + + + +

+ {i18n.translate('visTypeXy.splitChartWarning.title', { + defaultMessage: 'Warning', + })} +

+
+
+
+ + {i18n.translate('visTypeXy.splitChartWarning.content', { + defaultMessage: + 'Warning split chart aggregations are not supported with vis_type_xy plugin', + })} + +
+
+
+ ); +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_axis.tsx b/src/plugins/vis_type_xy/public/components/xy_axis.tsx new file mode 100644 index 0000000000000..274090d39308f --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_axis.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { Axis } from '@elastic/charts'; + +import { AxisConfig } from '../types'; + +type XYAxisPros = AxisConfig; + +export const XYAxis: FC = ({ + id, + title, + show, + position, + groupId, + grid, + ticks, + domain, + style, +}) => { + return ( + + ); +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_current_time.tsx b/src/plugins/vis_type_xy/public/components/xy_current_time.tsx new file mode 100644 index 0000000000000..48df1549e48ac --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_current_time.tsx @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; +import { DomainRange } from '@elastic/charts'; +import { CurrentTime } from '../../../charts/public'; + +interface XYCurrentTime { + enabled: boolean; + isDarkMode: boolean; + domain: DomainRange; +} + +export const XYCurrentTime: FC = ({ enabled, isDarkMode, domain }) => { + if (!enabled) { + return null; + } + + const domainEnd = 'max' in domain ? domain.max : undefined; + return ; +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_endzones.tsx b/src/plugins/vis_type_xy/public/components/xy_endzones.tsx new file mode 100644 index 0000000000000..3ab4fb66d6df8 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_endzones.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; +import { isUndefined } from 'lodash'; + +import { DomainRange } from '@elastic/charts'; + +import { Endzones } from '../../../charts/public'; + +interface XYEndzones { + enabled: boolean; + isDarkMode: boolean; + groupId?: string; + domain: DomainRange; + adjustedDomain: DomainRange; +} + +export const XYEndzones: FC = ({ + enabled, + isDarkMode, + groupId, + domain, + adjustedDomain, +}) => { + if ( + enabled && + 'min' in domain && + 'max' in domain && + !isUndefined(domain.minInterval) && + 'min' in adjustedDomain && + 'max' in adjustedDomain + ) { + return ( + + ); + } + + return null; +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx new file mode 100644 index 0000000000000..351d0892094c0 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { + Direction, + Settings, + DomainRange, + Position, + PartialTheme, + ElementClickListener, + BrushEndListener, + LegendAction, + LegendColorPicker, +} from '@elastic/charts'; + +import { renderEndzoneTooltip } from '../../../charts/public'; + +import { getThemeService } from '../services'; +import { VisConfig } from '../types'; + +type XYSettingsProps = Pick< + VisConfig, + | 'markSizeRatio' + | 'rotation' + | 'enableHistogramMode' + | 'tooltip' + | 'isTimeChart' + | 'xAxis' + | 'orderBucketsBySum' +> & { + xDomain: DomainRange; + adjustedXDomain: DomainRange; + showLegend: boolean; + onElementClick: ElementClickListener; + onBrushEnd: BrushEndListener; + legendAction: LegendAction; + legendColorPicker: LegendColorPicker; + legendPosition: Position; +}; + +export const XYSettings: FC = ({ + markSizeRatio, + rotation, + enableHistogramMode, + tooltip, + isTimeChart, + xAxis, + orderBucketsBySum, + xDomain, + adjustedXDomain, + showLegend, + onElementClick, + onBrushEnd, + legendAction, + legendColorPicker, + legendPosition, +}) => { + const themeService = getThemeService(); + const theme = themeService.useChartsTheme(); + const baseTheme = themeService.useChartsBaseTheme(); + const themeOverrides: PartialTheme = { + markSizeRatio, + chartMargins: + legendPosition === Position.Top || legendPosition === Position.Right + ? { + bottom: 10, + } + : { + right: 10, + }, + }; + + return ( + + ); +}; diff --git a/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx new file mode 100644 index 0000000000000..46b0a009807a2 --- /dev/null +++ b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; + +import { AnnotationDomainTypes, LineAnnotation } from '@elastic/charts'; + +import { ThresholdLineConfig } from '../types'; + +type XYThresholdLineProps = ThresholdLineConfig & { + groupId?: string; +}; + +export const XYThresholdLine: FC = ({ + show, + value: dataValue, + color, + width, + groupId, + dash, +}) => { + if (!show) { + return null; + } + + return ( + + ); +}; diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts new file mode 100644 index 0000000000000..7a13d40c2ada2 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { compact } from 'lodash'; + +import { i18n } from '@kbn/i18n'; + +import { KibanaDatatableColumn } from '../../../expressions/public'; + +import { Aspect, Dimension, Aspects, Dimensions } from '../types'; +import { getFormatService } from '../services'; + +export function getEmptyAspect(): Aspect { + return { + accessor: null, + aggId: null, + title: i18n.translate('visTypeXY.aggResponse.allDocsTitle', { + defaultMessage: 'All docs', + }), + params: { + defaultValue: '_all', + }, + }; +} +export function getAspects( + columns: KibanaDatatableColumn[], + { x, y, z, series }: Dimensions +): Aspects { + const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series]; + + return { + x: getAspectsFromDimension(columns, x) ?? getEmptyAspect(), + y: getAspectsFromDimension(columns, y) ?? [], + z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined, + series: getAspectsFromDimension(columns, seriesDimensions), + }; +} +function getAspectsFromDimension( + columns: KibanaDatatableColumn[], + dimension?: Dimension | null +): Aspect | undefined; +function getAspectsFromDimension( + columns: KibanaDatatableColumn[], + dimensions?: Dimension[] | null +): Aspect[] | undefined; +function getAspectsFromDimension( + columns: KibanaDatatableColumn[], + dimensions?: Dimension | Dimension[] | null +): Aspect[] | Aspect | undefined { + if (!dimensions) { + return; + } + + if (Array.isArray(dimensions)) { + return compact( + dimensions.map((d) => { + const column = d && columns[d.accessor]; + return column && getAspect(column, d); + }) + ); + } + + const column = columns[dimensions.accessor]; + return column && getAspect(column, dimensions); +} +const getAspect = ( + { id: accessor, name: title }: KibanaDatatableColumn, + { accessor: column, format, params }: Dimension +): Aspect => ({ + accessor, + column, + title, + format, + // TODO: Change this to be more clear. Now it's based on how _add_to_siri creates id for params + // src/legacy/ui/public/agg_response/point_series/_add_to_siri.js + aggId: (accessor ?? '').split('-').pop() ?? null, + formatter: (value: any) => getFormatService().deserialize(format).convert(value), + params, +}); diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts new file mode 100644 index 0000000000000..292662b8374bb --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_axis.ts @@ -0,0 +1,160 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxisSpec, TickFormatter, YDomainRange, ScaleType as ECScaleType } from '@elastic/charts'; + +import { + Aspect, + CategoryAxis, + Grid, + AxisConfig, + TickOptions, + ScaleConfig, + Scale, + ScaleType, + AxisType, + XScaleType, + YScaleType, +} from '../types'; + +export function getAxis( + { type, title: axisTitle, labels, scale: axisScale, ...axis }: CategoryAxis, + { categoryLines, valueAxis }: Grid, + { params, formatter, title: fallbackTitle = '' }: Aspect +): AxisConfig { + const isCategoryAxis = type === AxisType.Category; + const groupId = isCategoryAxis ? axis.id : undefined; + const grid = isCategoryAxis + ? { + show: categoryLines, + } + : { + show: valueAxis === axis.id, + }; + const ticks: TickOptions = { + formatter, + labelFormatter: getLabelFormatter(labels.truncate, formatter), + show: labels.show, + rotation: labels.rotate, + showOverlappingLabels: !labels.filter, + showDuplicates: !labels.filter, + }; + const scale = getScale(axisScale, params, isCategoryAxis); + const title = axisTitle.text || fallbackTitle; + + return { + ...axis, + groupId, + title, + ticks, + grid, + scale, + style: getAxisStyle(ticks, title), + domain: getAxisDomain(scale, isCategoryAxis), + }; +} + +function getLabelFormatter( + truncate?: number | null, + formatter?: TickFormatter +): TickFormatter | undefined { + if (truncate === null || truncate === undefined) { + return formatter; + } + + return (value: any) => { + const formattedValue = formatter ? formatter(value) : value; + + if (formattedValue.length > truncate) { + return `${formattedValue.slice(0, truncate)}...`; + } + + return formattedValue; + }; +} + +function getScaleType(scale?: Scale, isTime = false, isHistogram = false): ECScaleType | undefined { + if (isTime) return 'time'; + if (isHistogram) return 'linear'; + + const type = scale?.type; + if (type === ScaleType.SquareRoot) { + return ECScaleType.Sqrt; + } + + return type ? type : undefined; +} + +function getScale( + scale: Scale, + params: Aspect['params'], + isCategoryAxis: boolean +): ScaleConfig { + const type = (isCategoryAxis + ? getScaleType(scale, 'date' in params, 'interval' in params) + : getScaleType(scale)) as S; + + return { + ...scale, + type, + }; +} + +function getAxisStyle(ticks?: TickOptions, title?: string): AxisSpec['style'] { + return { + axisTitle: { + visible: (title ?? '').trim().length > 0, + }, + tickLabel: { + visible: ticks?.show, + rotation: !ticks?.rotation ? ticks?.rotation : -ticks?.rotation, + }, + }; +} + +function getAxisDomain( + scale: ScaleConfig, + isCategoryAxis: boolean +): YDomainRange | undefined { + if (isCategoryAxis || !scale) { + return; + } + + const { min, max, defaultYExtents, boundsMargin } = scale; + const fit = defaultYExtents; + const padding = boundsMargin; + + if (isDefined(min) && isDefined(max)) { + return { fit, padding, min, max }; + } + + if (isDefined(min)) { + return { fit, padding, min }; + } + + if (isDefined(max)) { + return { fit, padding, max }; + } + + return { fit, padding }; +} + +function isDefined(value: T): value is NonNullable { + return value !== undefined && value !== null; +} diff --git a/src/plugins/vis_type_xy/public/config/get_config.ts b/src/plugins/vis_type_xy/public/config/get_config.ts new file mode 100644 index 0000000000000..6b6c5e0667ed2 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_config.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaDatatable } from '../../../expressions/public'; + +import { DateHistogramParams, VisConfig, VisParams, XScaleType, YScaleType } from '../types'; +import { getThresholdLine } from './get_threshold_line'; +import { getRotation } from './get_rotation'; +import { getTooltip } from './get_tooltip'; +import { getLegend } from './get_legend'; +import { getAxis } from './get_axis'; +import { getAspects } from './get_aspects'; + +export function getConfig(table: KibanaDatatable, params: VisParams): VisConfig { + const { thresholdLine, orderBucketsBySum, addTimeMarker, radiusRatio, labels } = params; + const aspects = getAspects(table.columns, params.dimensions); + const xAxis = getAxis(params.categoryAxes[0], params.grid, aspects.x); + const tooltip = getTooltip(params, xAxis.ticks?.formatter); + const enableHistogramMode = ['date_histogram', 'histogram'].includes( + params.dimensions.x?.aggType ?? '' + ); + const fallbackGroupId = params.seriesParams.find(({ valueAxis }) => + params.valueAxes.some(({ id }) => valueAxis === id) + )?.valueAxis; + // TODO get formatter for each value axis + const yAxes = params.valueAxes + .map((a) => getAxis(a, params.grid, aspects.y[0])) + .map(({ groupId, ...rest }) => ({ + ...rest, + // TODO remove when allowed to display unassigned axes + groupId: params.seriesParams.some(({ valueAxis }) => valueAxis === groupId) + ? groupId + : fallbackGroupId, + })); + const isTimeChart = (aspects.x.params as DateHistogramParams).date ?? false; + + return { + // NOTE: downscale ratio to match current vislib implementation + markSizeRatio: radiusRatio * 0.6, + orderBucketsBySum, + isTimeChart, + showCurrentTime: addTimeMarker && isTimeChart, + showValueLabel: labels.show ?? false, + enableHistogramMode, + tooltip, + aspects, + xAxis, + yAxes, + legend: getLegend(params), + rotation: getRotation(params.categoryAxes[0]), + thresholdLine: getThresholdLine(thresholdLine, yAxes, params.seriesParams), + }; +} diff --git a/src/plugins/vis_type_xy/public/config/get_legend.ts b/src/plugins/vis_type_xy/public/config/get_legend.ts new file mode 100644 index 0000000000000..a376ec3c4fb96 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_legend.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegendOptions, VisParams } from '../types'; + +export function getLegend({ addLegend, legendPosition }: VisParams): LegendOptions { + return { + show: addLegend, + position: legendPosition, + }; +} diff --git a/src/plugins/vis_type_xy/public/config/get_rotation.ts b/src/plugins/vis_type_xy/public/config/get_rotation.ts new file mode 100644 index 0000000000000..3c1e9a8f9130a --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_rotation.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Rotation } from '@elastic/charts'; + +import { CategoryAxis } from '../types'; + +export function getRotation({ position }: CategoryAxis): Rotation { + if (position === 'left' || position === 'right') { + return 90; + } + + return 0; +} diff --git a/src/plugins/vis_type_xy/public/config/get_threshold_line.ts b/src/plugins/vis_type_xy/public/config/get_threshold_line.ts new file mode 100644 index 0000000000000..861769f829074 --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_threshold_line.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ThresholdLineConfig, + ThresholdLine, + ThresholdLineStyle, + AxisConfig, + SeriesParam, + YScaleType, +} from '../types'; + +export function getThresholdLine( + { style, ...rest }: ThresholdLine, + yAxes: Array>, + seriesParams: SeriesParam[] +): ThresholdLineConfig { + const groupId = yAxes.find(({ id }) => seriesParams.some(({ valueAxis }) => id === valueAxis)) + ?.groupId; + + return { + ...rest, + dash: getDash(style), + groupId, + }; +} + +function getDash(style: ThresholdLineStyle): number[] | undefined { + switch (style) { + case ThresholdLineStyle.Dashed: + return [10, 5]; + case ThresholdLineStyle.DotDashed: + return [20, 5, 5, 5]; + case ThresholdLineStyle.Full: + default: + return; + } +} diff --git a/src/plugins/vis_type_xy/public/config/get_tooltip.ts b/src/plugins/vis_type_xy/public/config/get_tooltip.ts new file mode 100644 index 0000000000000..c40c74604b9ad --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/get_tooltip.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TooltipProps, TooltipType, TooltipValueFormatter, TickFormatter } from '@elastic/charts'; + +import { VisParams } from '../types'; + +export function getTooltip( + { addTooltip, detailedTooltip }: VisParams, + formatter?: TickFormatter +): TooltipProps { + // let type: TooltipType = TooltipType.None; + // if (addTooltip) { + // if (detailedTooltip) { + // type = TooltipType.Follow; + // } else { + // type = TooltipType.VerticalCursor; + // } + // } + const type = addTooltip ? TooltipType.VerticalCursor : TooltipType.None; + const headerFormatter: TooltipValueFormatter | undefined = formatter + ? ({ value }) => formatter(value) + : undefined; + + return { + type, + headerFormatter, + }; +} diff --git a/src/plugins/vis_type_xy/public/config/index.ts b/src/plugins/vis_type_xy/public/config/index.ts new file mode 100644 index 0000000000000..181ee02e2020b --- /dev/null +++ b/src/plugins/vis_type_xy/public/config/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getConfig } from './get_config'; diff --git a/src/plugins/vis_type_xy/public/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/detailed_tooltip.tsx new file mode 100644 index 0000000000000..cb6e4f66253cd --- /dev/null +++ b/src/plugins/vis_type_xy/public/detailed_tooltip.tsx @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +export const DetailedTooltip = () => { + return
; +}; diff --git a/src/plugins/vis_type_xy/public/editor/collections.ts b/src/plugins/vis_type_xy/public/editor/collections.ts new file mode 100644 index 0000000000000..5dc42079bc7dd --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/collections.ts @@ -0,0 +1,210 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Position } from '@elastic/charts'; + +import { AxisMode, ChartMode, InterpolationMode, ThresholdLineStyle, ScaleType } from '../types'; +import { ChartType } from '../../common/types'; +import { LabelRotation } from '../../../charts/public'; + +export const getPositions = () => [ + { + text: i18n.translate('visTypeXy.legendPositions.topText', { + defaultMessage: 'Top', + }), + value: Position.Top, + }, + { + text: i18n.translate('visTypeXy.legendPositions.leftText', { + defaultMessage: 'Left', + }), + value: Position.Left, + }, + { + text: i18n.translate('visTypeXy.legendPositions.rightText', { + defaultMessage: 'Right', + }), + value: Position.Right, + }, + { + text: i18n.translate('visTypeXy.legendPositions.bottomText', { + defaultMessage: 'Bottom', + }), + value: Position.Bottom, + }, +]; + +export const getChartTypes = () => [ + { + text: i18n.translate('visTypeXy.chartTypes.lineText', { + defaultMessage: 'Line', + }), + value: ChartType.Line, + }, + { + text: i18n.translate('visTypeXy.chartTypes.areaText', { + defaultMessage: 'Area', + }), + value: ChartType.Area, + }, + { + text: i18n.translate('visTypeXy.chartTypes.barText', { + defaultMessage: 'Bar', + }), + value: ChartType.Histogram, + }, +]; + +export const getChartModes = () => [ + { + text: i18n.translate('visTypeXy.chartModes.normalText', { + defaultMessage: 'Normal', + }), + value: ChartMode.Normal, + }, + { + text: i18n.translate('visTypeXy.chartModes.stackedText', { + defaultMessage: 'Stacked', + }), + value: ChartMode.Stacked, + }, +]; + +export const getInterpolationModes = () => [ + { + text: i18n.translate('visTypeXy.interpolationModes.straightText', { + defaultMessage: 'Straight', + }), + value: InterpolationMode.Linear, + }, + { + text: i18n.translate('visTypeXy.interpolationModes.smoothedText', { + defaultMessage: 'Smoothed', + }), + value: InterpolationMode.Cardinal, + }, + { + text: i18n.translate('visTypeXy.interpolationModes.steppedText', { + defaultMessage: 'Stepped', + }), + value: InterpolationMode.StepAfter, + }, +]; + +export const getScaleTypes = () => [ + { + 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, + }, +]; + +export const getAxisModes = () => [ + { + text: i18n.translate('visTypeXy.axisModes.normalText', { + defaultMessage: 'Normal', + }), + value: AxisMode.Normal, + }, + { + text: i18n.translate('visTypeXy.axisModes.percentageText', { + defaultMessage: 'Percentage', + }), + value: AxisMode.Percentage, + }, + { + text: i18n.translate('visTypeXy.axisModes.wiggleText', { + defaultMessage: 'Wiggle', + }), + value: AxisMode.Wiggle, + }, + { + text: i18n.translate('visTypeXy.axisModes.silhouetteText', { + defaultMessage: 'Silhouette', + }), + value: AxisMode.Silhouette, + }, +]; + +export const getThresholdLineStyles = () => [ + { + value: ThresholdLineStyle.Full, + text: i18n.translate('visTypeXy.thresholdLine.style.fullText', { + defaultMessage: 'Full', + }), + }, + { + value: ThresholdLineStyle.Dashed, + text: i18n.translate('visTypeXy.thresholdLine.style.dashedText', { + defaultMessage: 'Dashed', + }), + }, + { + value: ThresholdLineStyle.DotDashed, + text: i18n.translate('visTypeXy.thresholdLine.style.dotdashedText', { + defaultMessage: 'Dot-dashed', + }), + }, +]; + +export const getRotateOptions = () => [ + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.horizontalText', { + defaultMessage: 'Horizontal', + }), + value: LabelRotation.Horizontal, + }, + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.verticalText', { + defaultMessage: 'Vertical', + }), + value: LabelRotation.Vertical, + }, + { + text: i18n.translate('visTypeXy.categoryAxis.rotate.angledText', { + defaultMessage: 'Angled', + }), + value: LabelRotation.Angled, + }, +]; + +export const getConfigCollections = () => ({ + legendPositions: getPositions(), + positions: getPositions(), + chartTypes: getChartTypes(), + axisModes: getAxisModes(), + scaleTypes: getScaleTypes(), + chartModes: getChartModes(), + interpolationModes: getInterpolationModes(), + thresholdLineStyles: getThresholdLineStyles(), +}); diff --git a/src/plugins/vis_type_vislib/public/utils/common_config.tsx b/src/plugins/vis_type_xy/public/editor/common_config.tsx similarity index 63% rename from src/plugins/vis_type_vislib/public/utils/common_config.tsx rename to src/plugins/vis_type_xy/public/editor/common_config.tsx index de867dc72bba7..28075eb774a9d 100644 --- a/src/plugins/vis_type_vislib/public/utils/common_config.tsx +++ b/src/plugins/vis_type_xy/public/editor/common_config.tsx @@ -20,36 +20,31 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { PointSeriesOptions, MetricsAxisOptions } from '../components/options'; -import { ValidationWrapper } from '../components/common'; -import { BasicVislibParams } from '../types'; +import { VisOptionsProps } from '../../../vis_default_editor/public'; -function getAreaOptionTabs() { +import { VisParams } from '../types'; +import { MetricsAxisOptions, PointSeriesOptions } from './components/options'; +import { ValidationWrapper } from './components/common'; + +export function getOptionTabs() { return [ { name: 'advanced', - title: i18n.translate('visTypeVislib.area.tabs.metricsAxesTitle', { + title: i18n.translate('visTypeXy.area.tabs.metricsAxesTitle', { defaultMessage: 'Metrics & axes', }), - editor: (props: VisOptionsProps) => ( + editor: (props: VisOptionsProps) => ( ), }, { name: 'options', - title: i18n.translate('visTypeVislib.area.tabs.panelSettingsTitle', { + title: i18n.translate('visTypeXy.area.tabs.panelSettingsTitle', { defaultMessage: 'Panel settings', }), - editor: (props: VisOptionsProps) => ( + editor: (props: VisOptionsProps) => ( ), }, ]; } - -const countLabel = i18n.translate('visTypeVislib.area.countText', { - defaultMessage: 'Count', -}); - -export { getAreaOptionTabs, countLabel }; diff --git a/src/plugins/vis_type_vislib/public/components/common/index.ts b/src/plugins/vis_type_xy/public/editor/components/common/index.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/components/common/index.ts rename to src/plugins/vis_type_xy/public/editor/components/common/index.ts diff --git a/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx b/src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx similarity index 95% rename from src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx rename to src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx index 2f0cb701848d0..162ae8f9517c2 100644 --- a/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/common/truncate_labels.tsx @@ -33,7 +33,7 @@ function TruncateLabelsOption({ disabled, value = null, setValue }: TruncateLabe return ( extends VisOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; diff --git a/src/plugins/vis_type_vislib/public/components/index.ts b/src/plugins/vis_type_xy/public/editor/components/index.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/components/index.ts rename to src/plugins/vis_type_xy/public/editor/components/index.ts diff --git a/src/plugins/vis_type_vislib/public/components/options/index.ts b/src/plugins/vis_type_xy/public/editor/components/options/index.ts similarity index 88% rename from src/plugins/vis_type_vislib/public/components/options/index.ts rename to src/plugins/vis_type_xy/public/editor/components/options/index.ts index 57afbd4818ae4..feed5b07bf7b5 100644 --- a/src/plugins/vis_type_vislib/public/components/options/index.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/index.ts @@ -17,8 +17,5 @@ * under the License. */ -export { GaugeOptions } from './gauge'; -export { PieOptions } from './pie'; export { PointSeriesOptions } from './point_series'; -export { HeatmapOptions } from './heatmap'; export { MetricsAxisOptions } from './metrics_axes'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap similarity index 100% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap similarity index 99% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 09e0753d592e5..c5ce09d4d78af 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -74,7 +74,6 @@ exports[`MetricsAxisOptions component should init with the default set of props /> { let setCategoryAxis: jest.Mock; let onPositionChanged: jest.Mock; let defaultProps: CategoryAxisPanelProps; - let axis: Axis; + let axis: CategoryAxis; beforeEach(() => { setCategoryAxis = jest.fn(); @@ -60,7 +60,7 @@ describe('CategoryAxisPanel component', () => { }); it('should call onPositionChanged when position is changed', () => { - const value = Positions.RIGHT; + const value = Position.Right; const comp = shallow(); comp.find({ paramName: 'position' }).prop('setValue')('position', value); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx similarity index 78% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx index 468fb1f8c315a..a551163747526 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx @@ -19,20 +19,21 @@ import React, { useCallback } from 'react'; -import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { Position } from '@elastic/charts'; + +import { SelectOption, SwitchOption } from '../../../../../../charts/public'; +import { VisOptionsProps } from '../../../../../../vis_default_editor/public'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { Axis } from '../../../types'; -import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; -import { Positions } from '../../../utils/collections'; +import { CategoryAxis } from '../../../../types'; export interface CategoryAxisPanelProps { - axis: Axis; - onPositionChanged: (position: Positions) => void; - setCategoryAxis: (value: Axis) => void; + axis: CategoryAxis; + onPositionChanged: (position: Position) => void; + setCategoryAxis: (value: CategoryAxis) => void; vis: VisOptionsProps['vis']; } @@ -43,7 +44,7 @@ function CategoryAxisPanel({ setCategoryAxis, }: CategoryAxisPanelProps) { const setAxis = useCallback( - (paramName: T, value: Axis[T]) => { + (paramName: T, value: CategoryAxis[T]) => { const updatedAxis = { ...axis, [paramName]: value, @@ -54,7 +55,7 @@ function CategoryAxisPanel({ ); const setPosition = useCallback( - (paramName: 'position', value: Axis['position']) => { + (paramName: 'position', value: CategoryAxis['position']) => { setAxis(paramName, value); onPositionChanged(value); }, @@ -77,7 +78,7 @@ function CategoryAxisPanel({

@@ -85,7 +86,7 @@ function CategoryAxisPanel({ { let setParamByIndex: jest.Mock; @@ -53,14 +54,14 @@ describe('ChartOptions component', () => { }); it('should show LineOptions when type is line', () => { - chart.type = ChartTypes.LINE; + chart.type = ChartType.Line; const comp = shallow(); expect(comp.find(LineOptions).exists()).toBeTruthy(); }); it('should show line mode when type is area', () => { - chart.type = ChartTypes.AREA; + chart.type = ChartType.Area; const comp = shallow(); expect(comp.find({ paramName: 'interpolate' }).exists()).toBeTruthy(); @@ -78,8 +79,8 @@ describe('ChartOptions component', () => { it('should call setParamByIndex when mode is changed', () => { const comp = shallow(); const paramName = 'mode'; - comp.find({ paramName }).prop('setValue')(paramName, ChartModes.NORMAL); + comp.find({ paramName }).prop('setValue')(paramName, ChartMode.Normal); - expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartModes.NORMAL); + expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Normal); }); }); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx similarity index 80% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index 623a8d1f348e9..728ddd27a4fc9 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -22,12 +22,13 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../visualizations/public'; -import { SeriesParam, ValueAxis } from '../../../types'; -import { ChartTypes } from '../../../utils/collections'; -import { SelectOption } from '../../../../../charts/public'; +import { Vis } from '../../../../../../visualizations/public'; +import { SelectOption } from '../../../../../../charts/public'; + +import { SeriesParam, ValueAxis } from '../../../../types'; import { LineOptions } from './line_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; +import { ChartType } from '../../../../../common/types'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; @@ -69,7 +70,7 @@ function ChartOptions({ value: id, })), { - text: i18n.translate('visTypeVislib.controls.pointSeries.series.newAxisLabel', { + text: i18n.translate('visTypeXy.controls.pointSeries.series.newAxisLabel', { defaultMessage: 'New axis…', }), value: 'new', @@ -82,7 +83,7 @@ function ChartOptions({ <> - {chart.type === ChartTypes.AREA && ( + {chart.type === ChartType.Area && ( <> )} - {chart.type === ChartTypes.LINE && ( - - )} + {chart.type === ChartType.Line && } ); } diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx similarity index 99% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx index 4798c67928f7f..82097271f8814 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.test.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; + import { CustomExtentsOptions, CustomExtentsOptionsProps } from './custom_extents_options'; import { YExtents } from './y_extents'; import { valueAxis } from './mocks'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx similarity index 90% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx index 634d6b3f0641c..58ca5300469ec 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/custom_extents_options.tsx @@ -18,10 +18,12 @@ */ import React, { useCallback, useEffect } from 'react'; + import { i18n } from '@kbn/i18n'; -import { ValueAxis } from '../../../types'; -import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../../charts/public'; + +import { ValueAxis } from '../../../../types'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; @@ -39,7 +41,7 @@ function CustomExtentsOptions({ setValueAxisScale, }: CustomExtentsOptionsProps) { const invalidBoundsMarginMessage = i18n.translate( - 'visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', + 'visTypeXy.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin', { defaultMessage: 'Bounds margin must be greater than or equal to 0.' } ); @@ -85,7 +87,7 @@ function CustomExtentsOptions({ <> ({ + SeriesPanel: () => 'SeriesPanel', +})); +jest.mock('./category_axis_panel', () => ({ + CategoryAxisPanel: () => 'CategoryAxisPanel', +})); +jest.mock('./value_axes_panel', () => ({ + ValueAxesPanel: () => 'ValueAxesPanel', +})); + +const SERIES_PARAMS = 'seriesParams'; +const VALUE_AXES = 'valueAxes'; + +const aggCount: IAggConfig = { + id: '1', + type: { name: 'count' }, + makeLabel: () => 'Count', +} as IAggConfig; + +const aggAverage: IAggConfig = { + id: '2', + type: { name: 'average' } as IAggType, + makeLabel: () => 'Average', +} as IAggConfig; + +const createAggs = (aggs: any[]) => ({ + aggs, + bySchemaName: () => aggs, +}); + +describe('MetricsAxisOptions component', () => { + let setValue: jest.Mock; + let defaultProps: ValidationVisOptionsProps; + let axis: ValueAxis; + let axisRight: ValueAxis; + let chart: SeriesParam; + + beforeEach(() => { + setValue = jest.fn(); + + axis = { + ...valueAxis, + name: 'LeftAxis-1', + position: Position.Left, + }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + name: 'RightAxis-1', + position: Position.Right, + }; + chart = { + ...seriesParam, + type: ChartType.Area, + }; + + defaultProps = { + aggs: createAggs([aggCount]), + isTabSelected: true, + vis: { + type: { + type: ChartType.Area, + schemas: { metrics: [{ name: 'metric' }] }, + }, + setState: jest.fn(), + serialize: jest.fn(), + }, + stateParams: { + valueAxes: [axis], + seriesParams: [chart], + categoryAxes: [categoryAxis], + grid: { + valueAxis: defaultValueAxisId, + }, + }, + setValue, + } as any; + }); + + it('should init with the default set of props', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + describe('useEffect', () => { + it('should update series when new agg is added', () => { + const component = mount(); + component.setProps({ + aggs: createAggs([aggCount, aggAverage]), + }); + + const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update series when new agg label is changed', () => { + const component = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'New label' }; + component.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update visType when one seriesParam', () => { + const component = mount(); + expect(defaultProps.vis.type.type).toBe(ChartType.Area); + + component.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [{ ...chart, type: ChartType.Line }], + }, + }); + + expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartType.Line }); + }); + + it('should set histogram visType when multiple seriesParam', () => { + const component = mount(); + expect(defaultProps.vis.type.type).toBe(ChartType.Area); + + component.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, { ...chart, type: ChartType.Line }], + }, + }); + + expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartType.Histogram }); + }); + }); + + describe('updateAxisTitle', () => { + it('should not update the value axis title if custom title was set', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const component = mount(); + const newAgg = { + ...aggCount, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([newAgg]), + }); + const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { + const component = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeriesParams = [{ ...chart, data: { ...chart.data, label: agg.makeLabel() } }]; + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + + expect(setValue).toHaveBeenCalledTimes(5); + expect(setValue).toHaveBeenNthCalledWith(3, SERIES_PARAMS, updatedSeriesParams); + expect(setValue).toHaveBeenNthCalledWith(5, SERIES_PARAMS, updatedSeriesParams); + expect(setValue).toHaveBeenNthCalledWith(4, VALUE_AXES, updatedValues); + }); + + it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { + const component = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; + component.setProps({ + aggs: createAggs([agg, aggAverage]), + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, chart], + }, + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const component = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + component.setProps({ + aggs: createAggs([agg]), + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + }); + + it('should add value axis', () => { + const component = shallow(); + component.find(ValueAxesPanel).prop('addValueAxis')(); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); + }); + + describe('removeValueAxis', () => { + beforeEach(() => { + defaultProps.stateParams.valueAxes = [axis, axisRight]; + }); + + it('should remove value axis', () => { + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); + }); + + it('should update seriesParams "valueAxis" prop', () => { + const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); + }); + + it('should reset grid "valueAxis" prop', () => { + const updatedGrid = { valueAxis: undefined }; + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const component = shallow(); + component.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); + }); + }); + + describe('onValueAxisPositionChanged', () => { + const getProps = ( + valuePosition1: Position = Position.Right, + valuePosition2: Position = Position.Left + ): ValidationVisOptionsProps => ({ + ...defaultProps, + stateParams: { + ...defaultProps.stateParams, + valueAxes: [ + { + ...valueAxis, + id: 'ValueAxis-1', + position: valuePosition1, + }, + { + ...valueAxis, + id: 'ValueAxis-2', + position: valuePosition2, + }, + { + ...valueAxis, + id: 'ValueAxis-3', + position: valuePosition2, + }, + ], + categoryAxes: [ + { + ...categoryAxis, + position: mapPosition(valuePosition1), + }, + ], + }, + }); + + it('should update all value axes if another value axis changes from horizontal to vertical', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(0, Position.Bottom); + expect(setValue).nthCalledWith(1, 'categoryAxes', [ + expect.objectContaining({ + id: 'CategoryAxis-1', + position: Position.Right, + }), + ]); + expect(setValue).nthCalledWith(2, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Bottom, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Top, + }), + ]); + }); + + it('should update all value axes if another value axis changes from vertical to horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(1, Position.Left); + expect(setValue).nthCalledWith(1, 'categoryAxes', [ + expect.objectContaining({ + id: 'CategoryAxis-1', + position: Position.Top, + }), + ]); + expect(setValue).nthCalledWith(2, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Right, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should update only changed value axis if value axis stays horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(0, Position.Left); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should update only changed value axis if value axis stays vertical', () => { + const component = mount(); + setValue.mockClear(); + const onValueAxisPositionChanged = component + .find(ValueAxesPanel) + .prop('onValueAxisPositionChanged'); + onValueAxisPositionChanged(1, Position.Top); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Bottom, + }), + ]); + }); + }); + + describe('onCategoryAxisPositionChanged', () => { + const getProps = ( + position: Position = Position.Bottom + ): ValidationVisOptionsProps => ({ + ...defaultProps, + stateParams: { + ...defaultProps.stateParams, + valueAxes: [ + { + ...valueAxis, + id: 'ValueAxis-1', + position: mapPosition(position), + }, + { + ...valueAxis, + id: 'ValueAxis-2', + position: mapPositionOpposite(mapPosition(position)), + }, + { + ...valueAxis, + id: 'ValueAxis-3', + position: mapPosition(position), + }, + ], + categoryAxes: [ + { + ...categoryAxis, + position, + }, + ], + }, + }); + + it('should update all value axes if category axis changes from horizontal to vertical', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Left); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Bottom, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Top, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Bottom, + }), + ]); + }); + + it('should update all value axes if category axis changes from vertical to horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Top); + expect(setValue).nthCalledWith(1, 'valueAxes', [ + expect.objectContaining({ + id: 'ValueAxis-1', + position: Position.Left, + }), + expect.objectContaining({ + id: 'ValueAxis-2', + position: Position.Right, + }), + expect.objectContaining({ + id: 'ValueAxis-3', + position: Position.Left, + }), + ]); + }); + + it('should not update value axes if category axis stays horizontal', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Top); + expect(setValue).not.toBeCalled(); + }); + + it('should not update value axes if category axis stays vertical', () => { + const component = mount(); + setValue.mockClear(); + const onPositionChanged = component.find(CategoryAxisPanel).prop('onPositionChanged'); + onPositionChanged(Position.Right); + expect(setValue).not.toBeCalled(); + }); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx similarity index 83% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx index d885f8fb0b12f..7836066728de0 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx @@ -19,10 +19,12 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep, uniq, get } from 'lodash'; + import { EuiSpacer } from '@elastic/eui'; -import { IAggConfig } from 'src/plugins/data/public'; -import { BasicVislibParams, ValueAxis, SeriesParam, Axis } from '../../../types'; +import { IAggConfig } from '../../../../../../data/public'; + +import { VisParams, ValueAxis, SeriesParam, CategoryAxis } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { SeriesPanel } from './series_panel'; import { CategoryAxisPanel } from './category_axis_panel'; @@ -34,6 +36,7 @@ import { getUpdatedAxisName, mapPositionOpposite, mapPosition, + mapPositionOpposingOpposite, } from './utils'; export type SetParamByIndex =

( @@ -51,11 +54,9 @@ export type ChangeValueAxis = ( const VALUE_AXIS_PREFIX = 'ValueAxis-'; -function MetricsAxisOptions(props: ValidationVisOptionsProps) { +function MetricsAxisOptions(props: ValidationVisOptionsProps) { const { stateParams, setValue, aggs, vis, isTabSelected } = props; - const [isCategoryAxisHorizontal, setIsCategoryAxisHorizontal] = useState(true); - const setParamByIndex: SetParamByIndex = useCallback( (axesName, index, paramName, value) => { const items = stateParams[axesName]; @@ -72,7 +73,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ); const setCategoryAxis = useCallback( - (value: Axis) => { + (value: CategoryAxis) => { const categoryAxes = [...stateParams.categoryAxes]; categoryAxes[0] = value; setValue('categoryAxes', categoryAxes); @@ -170,33 +171,58 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ); const onValueAxisPositionChanged = useCallback( - (index: number, value: ValueAxis['position']) => { + (index: number, axisPosition: ValueAxis['position']) => { + const isHorizontalAxis = isAxisHorizontal(axisPosition); const valueAxes = [...stateParams.valueAxes]; - const name = getUpdatedAxisName(value, valueAxes); + const name = getUpdatedAxisName(axisPosition, valueAxes); + const [categoryAxes] = stateParams.categoryAxes; - valueAxes[index] = { - ...valueAxes[index], - name, - position: value, - }; - setValue('valueAxes', valueAxes); + if (isAxisHorizontal(categoryAxes.position) === isHorizontalAxis) { + const updatedCategoryAxes = { + ...categoryAxes, + position: mapPosition(categoryAxes.position), + }; + + setValue('categoryAxes', [updatedCategoryAxes]); + + const oldPosition = valueAxes[index].position; + const newValueAxes = valueAxes.map(({ position, ...axis }, i) => ({ + ...axis, + position: + i === index + ? axisPosition + : mapPositionOpposingOpposite(position, oldPosition, axisPosition), + })); + setValue('valueAxes', newValueAxes); + } else { + valueAxes[index] = { + ...valueAxes[index], + name, + position: axisPosition, + }; + setValue('valueAxes', valueAxes); + } }, - [stateParams.valueAxes, setValue] + [stateParams.valueAxes, stateParams.categoryAxes, setValue] ); const onCategoryAxisPositionChanged = useCallback( - (chartPosition: Axis['position']) => { - const isChartHorizontal = isAxisHorizontal(chartPosition); - setIsCategoryAxisHorizontal(isAxisHorizontal(chartPosition)); - - stateParams.valueAxes.forEach((axis, index) => { - if (isAxisHorizontal(axis.position) === isChartHorizontal) { - const position = mapPosition(axis.position); - onValueAxisPositionChanged(index, position); - } - }); + (axisPosition: CategoryAxis['position']) => { + const isHorizontalAxis = isAxisHorizontal(axisPosition); + + if ( + stateParams.valueAxes.some( + ({ position }) => isAxisHorizontal(position) === isHorizontalAxis + ) + ) { + const newValueAxes = stateParams.valueAxes.map(({ position, ...axis }) => ({ + ...axis, + position: mapPosition(position), + })); + setValue('valueAxes', newValueAxes); + } }, - [stateParams.valueAxes, onValueAxisPositionChanged] + [setValue, stateParams.valueAxes] ); const addValueAxis = useCallback(() => { @@ -314,7 +340,6 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ( - paramName: T, - value: Axis['labels'][T] -) => void; +export type SetAxisLabel = (paramName: T, value: Labels[T]) => void; export interface LabelOptionsProps { - axisLabels: Axis['labels']; + axisLabels: Labels; axisFilterCheckboxName: string; setAxisLabel: SetAxisLabel; } function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: LabelOptionsProps) { const setAxisLabelRotate = useCallback( - (paramName: 'rotate', value: Axis['labels']['rotate']) => { + (paramName: 'rotate', value: Labels['rotate']) => { setAxisLabel(paramName, Number(value)); }, [setAxisLabel] @@ -54,7 +51,7 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe

@@ -62,7 +59,7 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe

@@ -58,7 +59,7 @@ function SeriesPanel({ seriesParams, ...chartProps }: SeriesPanelProps) { buttonContent={chart.data.label} buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" aria-label={i18n.translate( - 'visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel', + 'visTypeXy.controls.pointSeries.seriesAccordionAriaLabel', { defaultMessage: 'Toggle {agg} options', values: { agg: chart.data.label }, diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts similarity index 53% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts index 708e8cf15f029..408409192c6dd 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - import { upperFirst } from 'lodash'; -import { BasicVislibParams, ValueAxis, SeriesParam } from '../../../types'; -import { ChartModes, ChartTypes, InterpolationModes, Positions } from '../../../utils/collections'; +import { Position } from '@elastic/charts'; + +import { VisParams, ValueAxis, SeriesParam, ChartMode, InterpolationMode } from '../../../../types'; +import { ChartType } from '../../../../../common/types'; -const makeSerie = ( +export const makeSerie = ( id: string, label: string, defaultValueAxis: ValueAxis['id'], @@ -31,11 +32,11 @@ const makeSerie = ( const data = { id, label }; const defaultSerie = { show: true, - mode: ChartModes.NORMAL, - type: ChartTypes.LINE, + mode: ChartMode.Normal, + type: ChartType.Line, drawLinesBetweenPoints: true, showCircles: true, - interpolate: InterpolationModes.LINEAR, + interpolate: InterpolationMode.Linear, lineWidth: 2, valueAxis: defaultValueAxis, data, @@ -43,12 +44,12 @@ const makeSerie = ( return lastSerie ? { ...lastSerie, data } : defaultSerie; }; -const isAxisHorizontal = (position: Positions) => - [Positions.TOP, Positions.BOTTOM].includes(position as any); +export const isAxisHorizontal = (position: Position) => + [Position.Top, Position.Bottom].includes(position as any); const RADIX = 10; -function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { +export function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { return (value: number, axis: ValueAxis) => { const nameLength = axisName.length; if (axis[axisProp].substr(0, nameLength) === axisName) { @@ -63,9 +64,9 @@ function countNextAxisNumber(axisName: string, axisProp: 'id' | 'name' = 'id') { const AXIS_PREFIX = 'Axis-'; -const getUpdatedAxisName = ( +export const getUpdatedAxisName = ( axisPosition: ValueAxis['position'], - valueAxes: BasicVislibParams['valueAxes'] + valueAxes: VisParams['valueAxes'] ) => { const axisName = upperFirst(axisPosition) + AXIS_PREFIX; const nextAxisNameNumber = valueAxes.reduce(countNextAxisNumber(axisName, 'name'), 1); @@ -73,39 +74,56 @@ const getUpdatedAxisName = ( return `${axisName}${nextAxisNameNumber}`; }; -function mapPositionOpposite(position: Positions) { +/** + * Maps axis position to opposite position + * @param position + */ +export function mapPositionOpposite(position: Position) { switch (position) { - case Positions.BOTTOM: - return Positions.TOP; - case Positions.TOP: - return Positions.BOTTOM; - case Positions.LEFT: - return Positions.RIGHT; - case Positions.RIGHT: - return Positions.LEFT; + case Position.Bottom: + return Position.Top; + case Position.Top: + return Position.Bottom; + case Position.Left: + return Position.Right; + case Position.Right: + return Position.Left; default: throw new Error('Invalid legend position.'); } } -function mapPosition(position: Positions) { - switch (position) { - case Positions.BOTTOM: - return Positions.LEFT; - case Positions.TOP: - return Positions.RIGHT; - case Positions.LEFT: - return Positions.BOTTOM; - case Positions.RIGHT: - return Positions.TOP; +/** + * Maps axis position to new position or opposite of new position based on old position + * @param position + * @param oldPosition + * @param newPosition + */ +export function mapPositionOpposingOpposite( + position: Position, + oldPosition: Position, + newPosition: Position +) { + if (position === oldPosition) { + return newPosition; } + + return mapPositionOpposite(newPosition); } -export { - makeSerie, - isAxisHorizontal, - countNextAxisNumber, - getUpdatedAxisName, - mapPositionOpposite, - mapPosition, -}; +/** + * Maps axis position to opposite rotation position + * @param position + */ +export function mapPosition(position: Position) { + switch (position) { + case Position.Bottom: + return Position.Left; + case Position.Top: + return Position.Right; + case Position.Left: + return Position.Bottom; + case Position.Right: + return Position.Top; + } +} diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx similarity index 96% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx index c5dfebdf720d8..c0c78977d2cdc 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx @@ -19,11 +19,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; -import { ValueAxis, SeriesParam } from '../../../types'; -import { Positions } from '../../../utils/collections'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; +import { ValueAxis, SeriesParam } from '../../../../types'; import { valueAxis, seriesParam, vis } from './mocks'; +import { Position } from '@elastic/charts'; describe('ValueAxesPanel component', () => { let setParamByIndex: jest.Mock; @@ -47,7 +48,7 @@ describe('ValueAxesPanel component', () => { axisRight = { ...valueAxis, id: 'ValueAxis-2', - position: Positions.RIGHT, + position: Position.Right, }; seriesParamCount = { ...seriesParam }; seriesParamAverage = { @@ -63,7 +64,6 @@ describe('ValueAxesPanel component', () => { seriesParams: [seriesParamCount, seriesParamAverage], valueAxes: [axisLeft, axisRight], vis, - isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, addValueAxis, diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx similarity index 90% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx index 5f7d33b7f1f47..397704663ff1f 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx @@ -31,13 +31,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../visualizations/public'; -import { SeriesParam, ValueAxis } from '../../../types'; +import { Vis } from '../../../../../../visualizations/public'; + +import { SeriesParam, ValueAxis } from '../../../../types'; import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from '.'; export interface ValueAxesPanelProps { - isCategoryAxisHorizontal: boolean; addValueAxis: () => ValueAxis; removeValueAxis: (axis: ValueAxis) => void; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; @@ -64,7 +64,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const removeButtonTooltip = useMemo( () => - i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip', { + i18n.translate('visTypeXy.controls.pointSeries.valueAxes.removeButtonTooltip', { defaultMessage: 'Remove Y-axis', }), [] @@ -87,7 +87,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const addButtonTooltip = useMemo( () => - i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip', { + i18n.translate('visTypeXy.controls.pointSeries.valueAxes.addButtonTooltip', { defaultMessage: 'Add Y-axis', }), [] @@ -115,7 +115,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) {

@@ -146,7 +146,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { buttonClassName="eui-textTruncate" buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" aria-label={i18n.translate( - 'visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', + 'visTypeXy.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', { defaultMessage: 'Toggle {axisName} options', values: { axisName: axis.name }, @@ -160,7 +160,6 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { axis={axis} index={index} valueAxis={valueAxes[index]} - isCategoryAxisHorizontal={props.isCategoryAxisHorizontal} onValueAxisPositionChanged={props.onValueAxisPositionChanged} setParamByIndex={props.setParamByIndex} setMultipleValidity={props.setMultipleValidity} diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx similarity index 64% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx index 1977bdba6eadf..0b325198c3fe7 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx @@ -19,21 +19,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; -import { ValueAxis } from '../../../types'; -import { TextInputOption } from '../../../../../charts/public'; + +import { Position } from '@elastic/charts'; + +import { TextInputOption } from '../../../../../../charts/public'; + +import { ValueAxis, ScaleType } from '../../../../types'; import { LabelOptions } from './label_options'; -import { ScaleTypes, Positions } from '../../../utils/collections'; +import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; import { valueAxis, vis } from './mocks'; const POSITION = 'position'; -interface PositionOption { - text: string; - value: Positions; - disabled: boolean; -} - describe('ValueAxisOptions component', () => { let setParamByIndex: jest.Mock; let onValueAxisPositionChanged: jest.Mock; @@ -52,7 +49,6 @@ describe('ValueAxisOptions component', () => { index: 0, valueAxis, vis, - isCategoryAxisHorizontal: false, setParamByIndex, onValueAxisPositionChanged, setMultipleValidity, @@ -73,50 +69,8 @@ describe('ValueAxisOptions component', () => { expect(comp.find(LabelOptions).exists()).toBeFalsy(); }); - it('should only allow left and right value axis position when category axis is horizontal', () => { - defaultProps.isCategoryAxisHorizontal = true; - const comp = shallow(); - - const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); - - expect(options.length).toBe(4); - options.forEach(({ value, disabled }) => { - switch (value) { - case Positions.LEFT: - case Positions.RIGHT: - expect(disabled).toBeFalsy(); - break; - case Positions.TOP: - case Positions.BOTTOM: - expect(disabled).toBeTruthy(); - break; - } - }); - }); - - it('should only allow top and bottom value axis position when category axis is vertical', () => { - defaultProps.isCategoryAxisHorizontal = false; - const comp = shallow(); - - const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); - - expect(options.length).toBe(4); - options.forEach(({ value, disabled }) => { - switch (value) { - case Positions.LEFT: - case Positions.RIGHT: - expect(disabled).toBeTruthy(); - break; - case Positions.TOP: - case Positions.BOTTOM: - expect(disabled).toBeFalsy(); - break; - } - }); - }); - it('should call onValueAxisPositionChanged when position is changed', () => { - const value = Positions.RIGHT; + const value = Position.Right; const comp = shallow(); comp.find({ paramName: POSITION }).prop('setValue')(POSITION, value); @@ -135,7 +89,7 @@ describe('ValueAxisOptions component', () => { }); it('should call setValueAxis when scale value is changed', () => { - const scaleValue = ScaleTypes.SQUARE_ROOT; + const scaleValue = ScaleType.SquareRoot; const comp = shallow(); comp.find({ paramName: 'type' }).prop('setValue')('type', scaleValue); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx similarity index 76% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index b42d038267d77..4ab792142e83a 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -17,17 +17,16 @@ * under the License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { Vis } from '../../../../../visualizations/public'; -import { ValueAxis } from '../../../types'; -import { Positions } from '../../../utils/collections'; -import { SelectOption, SwitchOption, TextInputOption } from '../../../../../charts/public'; +import { Vis } from '../../../../../../visualizations/public'; +import { SelectOption, SwitchOption, TextInputOption } from '../../../../../../charts/public'; + +import { ValueAxis } from '../../../../types'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; -import { isAxisHorizontal } from './utils'; import { SetParamByIndex } from '.'; export type SetScale = ( @@ -38,7 +37,6 @@ export type SetScale = ( export interface ValueAxisOptionsParams { axis: ValueAxis; index: number; - isCategoryAxisHorizontal: boolean; onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; setParamByIndex: SetParamByIndex; valueAxis: ValueAxis; @@ -46,10 +44,9 @@ export interface ValueAxisOptionsParams { setMultipleValidity: (paramName: string, isValid: boolean) => void; } -function ValueAxisOptions({ +export function ValueAxisOptions({ axis, index, - isCategoryAxisHorizontal, valueAxis, vis, onValueAxisPositionChanged, @@ -105,34 +102,13 @@ function ValueAxisOptions({ [index, onValueAxisPositionChanged] ); - const isPositionDisabled = useCallback( - (position: Positions) => { - if (isCategoryAxisHorizontal) { - return isAxisHorizontal(position); - } - return [Positions.LEFT, Positions.RIGHT].includes(position as any); - }, - [isCategoryAxisHorizontal] - ); - - const positions = useMemo( - () => - vis.type.editorConfig.collections.positions.map( - (position: { text: string; value: Positions }) => ({ - ...position, - disabled: isPositionDisabled(position.value), - }) - ), - [vis.type.editorConfig.collections.positions, isPositionDisabled] - ); - return ( <> ); } - -export { ValueAxisOptions }; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx similarity index 94% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx index 7bd5c9efdf29d..c2af7f2ad921b 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.test.tsx @@ -19,9 +19,11 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; + +import { NumberInputOption } from '../../../../../../charts/public'; + +import { ScaleType } from '../../../../types'; import { YExtents, YExtentsProps } from './y_extents'; -import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../charts/public'; describe('YExtents component', () => { let setMultipleValidity: jest.Mock; @@ -35,7 +37,7 @@ describe('YExtents component', () => { defaultProps = { scale: { - type: ScaleTypes.LINEAR, + type: ScaleType.Linear, }, setMultipleValidity, setScale, @@ -81,7 +83,7 @@ describe('YExtents component', () => { it('should call setMultipleValidity with false when min equals 0 and scale is log', () => { defaultProps.scale.min = 0; defaultProps.scale.max = 1; - defaultProps.scale.type = ScaleTypes.LOG; + defaultProps.scale.type = ScaleType.Log; mount(); expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx similarity index 83% rename from src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx rename to src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx index c2aa917dd3a6f..f14fbfa989e90 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/y_extents.tsx @@ -21,15 +21,15 @@ import React, { useEffect, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Scale } from '../../../types'; -import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../charts/public'; +import { NumberInputOption } from '../../../../../../charts/public'; + +import { Scale, ScaleType } from '../../../../types'; import { SetScale } from './value_axis_options'; -const rangeError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage', { +const rangeError = i18n.translate('visTypeXy.controls.pointSeries.valueAxes.minErrorMessage', { defaultMessage: 'Min should be less than Max.', }); -const minError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText', { +const minError = i18n.translate('visTypeXy.controls.pointSeries.valueAxes.minNeededScaleText', { defaultMessage: 'Min must exceed 0 when a log scale is selected.', }); @@ -59,7 +59,7 @@ function YExtents({ scale, setScale, setMultipleValidity }: YExtentsProps) { errors.push(rangeError); } - if (type === ScaleTypes.LOG && (isNullOrUndefined(min) || min <= 0)) { + if (type === ScaleType.Log && (isNullOrUndefined(min) || min <= 0)) { errors.push(minError); } @@ -86,7 +86,7 @@ function YExtents({ scale, setScale, setMultipleValidity }: YExtentsProps) { ) { +function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { const setGrid = useCallback( - ( - paramName: T, - value: BasicVislibParams['grid'][T] - ) => setValue('grid', { ...stateParams.grid, [paramName]: value }), + (paramName: T, value: VisParams['grid'][T]) => + setValue('grid', { ...stateParams.grid, [paramName]: value }), [stateParams.grid, setValue] ); @@ -42,7 +41,7 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps

@@ -72,18 +71,15 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { +function PointSeriesOptions(props: ValidationVisOptionsProps) { const { stateParams, setValue, vis } = props; return ( @@ -37,7 +39,7 @@ function PointSeriesOptions(props: ValidationVisOptionsProps)

@@ -50,7 +52,7 @@ function PointSeriesOptions(props: ValidationVisOptionsProps) (agg) => agg.schema === 'segment' && agg.type.name === 'date_histogram' ) ? ( ) /> ) : ( ) /> )} - {vis.type.name === ChartTypes.HISTOGRAM && ( + {vis.type.name === ChartType.Histogram && ( ) { +}: ValidationVisOptionsProps) { const setThresholdLine = useCallback( - ( + ( paramName: T, - value: BasicVislibParams['thresholdLine'][T] + value: VisParams['thresholdLine'][T] ) => setValue('thresholdLine', { ...stateParams.thresholdLine, [paramName]: value }), [stateParams.thresholdLine, setValue] ); const setThresholdLineColor = useCallback( - (value: BasicVislibParams['thresholdLine']['color']) => setThresholdLine('color', value), + (value: VisParams['thresholdLine']['color']) => setThresholdLine('color', value), [setThresholdLine] ); const setThresholdLineValidity = useCallback( - (paramName: keyof BasicVislibParams['thresholdLine'], isValid: boolean) => + (paramName: keyof VisParams['thresholdLine'], isValid: boolean) => setMultipleValidity(`thresholdLine__${paramName}`, isValid), [setMultipleValidity] ); @@ -59,7 +61,7 @@ function ThresholdPanel({

@@ -67,7 +69,7 @@ function ThresholdPanel({ ; visualizations: VisualizationsStart; + data: DataPublicPluginStart; } -type VisTypeXyCoreSetup = CoreSetup; +type VisTypeXyCoreSetup = CoreSetup; /** @internal */ -export class VisTypeXyPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - +export class VisTypeXyPlugin + implements + Plugin< + VisTypeXyPluginSetup, + VisTypeXyPluginStart, + VisTypeXyPluginSetupDependencies, + VisTypeXyPluginStartDependencies + > { public async setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies @@ -65,23 +73,21 @@ export class VisTypeXyPlugin implements Plugin { 'The visTypeXy plugin is enabled\n\n', 'This may negatively alter existing vislib visualization configurations if saved.' ); - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - - const visTypeDefinitions: any[] = []; - const visFunctions: any = []; + setUISettings(core.uiSettings); + setThemeService(charts.theme); + setColorsService(charts.colors); - visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); - visTypeDefinitions.forEach((vis: any) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + [createVisTypeXyVisFn].forEach(expressions.registerFunction); + visTypesDefinitions.forEach(visualizations.createReactVisualization); return {}; } - public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { - // nothing to do here + public start(core: CoreStart, { data }: VisTypeXyPluginStartDependencies) { + setFormatService(data.fieldFormats); + setDataActions(data.actions); + setTimefilter(data.query.timefilter.timefilter); + + return {}; } } diff --git a/src/plugins/vis_type_xy/public/services.ts b/src/plugins/vis_type_xy/public/services.ts new file mode 100644 index 0000000000000..e5cfb987cd85c --- /dev/null +++ b/src/plugins/vis_type_xy/public/services.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from '../../../core/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +export const [getUISettings, setUISettings] = createGetterSetter( + 'xy core.uiSettings' +); + +export const [getDataActions, setDataActions] = createGetterSetter< + DataPublicPluginStart['actions'] +>('xy data.actions'); + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('xy data.fieldFormats'); + +export const [getTimefilter, setTimefilter] = createGetterSetter< + DataPublicPluginStart['query']['timefilter']['timefilter'] +>('xy data.query.timefilter.timefilter'); + +export const [getThemeService, setThemeService] = createGetterSetter( + 'xy charts.theme' +); + +export const [getColorsService, setColorsService] = createGetterSetter( + 'xy charts.color' +); diff --git a/src/plugins/vis_type_xy/public/to_expression.ts b/src/plugins/vis_type_xy/public/to_expression.ts new file mode 100644 index 0000000000000..f1ed3c9f03521 --- /dev/null +++ b/src/plugins/vis_type_xy/public/to_expression.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { get } from 'lodash'; + +import { ExpressionFn } from '../../visualizations/public'; +import { DateHistogramParams, Dimensions, HistogramParams } from './types'; + +export const toExpression: ExpressionFn = async (vis, params, schemas): Promise => { + 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().filter(({ enabled }) => enabled) ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === '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(); + } else if (xAgg.type.name === '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 visConfig = vis.params; + + (dimensions.y || []).forEach((yDimension) => { + const yAgg = responseAggs[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find( + (param: any) => param.data.id === yAgg.id + ); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis: any) => valueAxis.id === seriesParam.valueAxis + ); + if (get(usedValueAxis, 'scale.mode') === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + }); + + visConfig.dimensions = dimensions; + + const configStr = `visConfig='${JSON.stringify(visConfig) + .replace(/\\/g, `\\\\`) + .replace(/'/g, `\\'`)}' `; + return `xy type='${vis.type.name}' ${configStr}`; +}; diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts new file mode 100644 index 0000000000000..4561e561c0d04 --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + AxisSpec, + GridLineStyle, + Position, + Rotation, + SeriesScales, + TickFormatter, + TooltipProps, + YDomainRange, +} from '@elastic/charts'; + +import { Dimension, Scale, ThresholdLine } from './param'; + +export interface Column { + // -1 value can be in a fake X aspect + id: string | null; + // id: string | -1; + name: string; +} + +export interface Aspect { + accessor: Column['id']; + aggId: string | null; + column?: Dimension['accessor']; + title: Column['name']; + format?: Dimension['format']; + formatter?: TickFormatter; + params: Dimension['params']; +} + +export interface Aspects { + x: Aspect; + y: Aspect[]; + z?: Aspect; + series?: Aspect[]; +} + +export interface AxisGrid { + show?: boolean; + styles?: GridLineStyle; +} + +export interface TickOptions { + show?: boolean; + size?: number; + count?: number; + padding?: number; + formatter?: TickFormatter; + labelFormatter?: TickFormatter; + rotation?: number; + showDuplicates?: boolean; + integersOnly?: boolean; + showOverlappingTicks?: boolean; + showOverlappingLabels?: boolean; +} + +export type YScaleType = SeriesScales['yScaleType']; +export type XScaleType = SeriesScales['xScaleType']; + +export type ScaleConfig = Omit & { + type?: S; +}; + +export interface AxisConfig { + id: string; + groupId?: string; + position: Position; + ticks?: TickOptions; + show: boolean; + style: AxisSpec['style']; + scale: ScaleConfig; + domain?: YDomainRange; + title?: string; + grid?: AxisGrid; +} + +export interface LegendOptions { + show: boolean; + position?: Position; +} + +export type ThresholdLineConfig = Omit & { + dash?: number[]; + groupId?: string; +}; + +export interface VisConfig { + legend: LegendOptions; + tooltip: TooltipProps; + xAxis: AxisConfig; + yAxes: Array>; + aspects: Aspects; + rotation: Rotation; + thresholdLine: ThresholdLineConfig; + orderBucketsBySum?: boolean; + showCurrentTime: boolean; + isTimeChart: boolean; + markSizeRatio: number; + showValueLabel: boolean; + enableHistogramMode: boolean; +} diff --git a/src/plugins/vis_type_xy/public/types/constants.ts b/src/plugins/vis_type_xy/public/types/constants.ts new file mode 100644 index 0000000000000..f92c56e43413e --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/constants.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { $Values } from '@kbn/utility-types'; + +export const ChartMode = Object.freeze({ + Normal: 'normal' as const, + Stacked: 'stacked' as const, +}); +export type ChartMode = $Values; + +export const InterpolationMode = Object.freeze({ + Linear: 'linear' as const, + Cardinal: 'cardinal' as const, + StepAfter: 'step-after' as const, +}); +export type InterpolationMode = $Values; + +export const AxisType = Object.freeze({ + Category: 'category' as const, + Value: 'value' as const, +}); +export type AxisType = $Values; + +export const ScaleType = Object.freeze({ + Linear: 'linear' as const, + Log: 'log' as const, + SquareRoot: 'square root' as const, +}); +export type ScaleType = $Values; + +export const AxisMode = Object.freeze({ + Normal: 'normal' as const, + Percentage: 'percentage' as const, + Wiggle: 'wiggle' as const, + Silhouette: 'silhouette' as const, +}); +export type AxisMode = $Values; + +export const ThresholdLineStyle = Object.freeze({ + Full: 'full' as const, + Dashed: 'dashed' as const, + DotDashed: 'dot-dashed' as const, +}); +export type ThresholdLineStyle = $Values; + +export const ColorMode = Object.freeze({ + Background: 'Background' as const, + Labels: 'Labels' as const, + None: 'None' as const, +}); +export type ColorMode = $Values; diff --git a/src/plugins/vis_type_xy/public/types/index.ts b/src/plugins/vis_type_xy/public/types/index.ts new file mode 100644 index 0000000000000..791373def2018 --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './constants'; +export * from './config'; +export * from './param'; +export * from './vis_type'; diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts new file mode 100644 index 0000000000000..5179e4591de17 --- /dev/null +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Position } from '@elastic/charts'; + +import { ColorSchemas, Style, Labels } from '../../../charts/public'; +import { SchemaConfig } from '../../../visualizations/public'; + +import { ChartType } from '../../common/types'; +import { + ChartMode, + AxisMode, + AxisType, + InterpolationMode, + ScaleType, + ThresholdLineStyle, +} from './constants'; + +export interface CommonVislibParams { + addTooltip: boolean; + legendPosition: Position; +} + +export interface ColorSchemaVislibParams { + colorSchema: ColorSchemas; + invertColors: boolean; +} + +export interface Scale { + boundsMargin?: number | ''; + defaultYExtents?: boolean; + max?: number | null; + min?: number | null; + mode?: AxisMode; + setYExtents?: boolean; + type: ScaleType; +} + +export interface CategoryAxis { + id: string; + labels: Labels; + position: Position; + scale: Scale; + show: boolean; + title: { + text?: string; + }; + type: AxisType; + /** + * Used only for heatmap, here for consistent types when used in vis_type_vislib + * remove with vis_type_vislib + */ + style: Partial