diff --git a/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts index 3bf414cf02211..bb8feabb159cd 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts +++ b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts @@ -36,6 +36,7 @@ import chrome from 'ui/chrome'; const courierRequestHandlerProvider = CourierRequestHandlerProvider; const courierRequestHandler = courierRequestHandlerProvider().handler; +import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { ExpressionFunction } from '../../types'; import { KibanaContext, KibanaDatatable } from '../../common'; @@ -47,6 +48,7 @@ interface Arguments { index: string | null; metricsAtAllLevels: boolean; partialRows: boolean; + includeFormatHints: boolean; aggConfigs: string; } @@ -77,6 +79,11 @@ export const esaggs = (): ExpressionFunction = await courierRequestHandler({ + const response = await courierRequestHandler({ searchSource, aggs, timeRange: get(context, 'timeRange', null), @@ -115,10 +122,16 @@ export const esaggs = (): ExpressionFunction ({ - id: column.id, - name: column.name, - })), + columns: response.columns.map((column: any) => { + const cleanedColumn: KibanaDatatable['columns'][0] = { + id: column.id, + name: column.name, + }; + if (args.includeFormatHints) { + cleanedColumn.formatHint = createFormat(column.aggConfig); + } + return cleanedColumn; + }), }; }, }); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 83bb25f26e7d5..22e41dbe757b4 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -23,11 +23,8 @@ import { setBounds } from 'ui/agg_types/buckets/date_histogram'; import { SearchSource } from 'ui/courier'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; import moment from 'moment'; - -interface SchemaFormat { - id: string; - params?: any; -} +import { SerializedFieldFormat } from 'src/plugins/data/common'; +import { createFormat } from './utilities'; interface SchemaConfigParams { precision?: number; @@ -36,7 +33,7 @@ interface SchemaConfigParams { export interface SchemaConfig { accessor: number; - format: SchemaFormat | {}; + format: SerializedFieldFormat | {}; params: SchemaConfigParams; aggType: string; } @@ -78,37 +75,6 @@ const vislibCharts: string[] = [ ]; export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { - const createFormat = (agg: AggConfig): SchemaFormat => { - const format: SchemaFormat = agg.params.field ? agg.params.field.format.toJSON() : {}; - const formats: any = { - date_range: () => ({ id: 'string' }), - percentile_ranks: () => ({ id: 'percent' }), - count: () => ({ id: 'number' }), - cardinality: () => ({ id: 'number' }), - date_histogram: () => ({ - id: 'date', - params: { - pattern: agg.buckets.getScaledDateFormat(), - }, - }), - terms: () => ({ - id: 'terms', - params: { - id: format.id, - otherBucketLabel: agg.params.otherBucketLabel, - missingBucketLabel: agg.params.missingBucketLabel, - ...format.params, - }, - }), - range: () => ({ - id: 'range', - params: { id: format.id, ...format.params }, - }), - }; - - return formats[agg.type.name] ? formats[agg.type.name]() : format; - }; - const createSchemaConfig = (accessor: number, agg: AggConfig): SchemaConfig => { if (agg.type.name === 'date_histogram') { agg.params.timeRange = timeRange; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index 20128eb5a3a64..cec5ca39712d9 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { identity } from 'lodash'; import { AggConfig, Vis } from 'ui/vis'; +import { SerializedFieldFormat } from 'src/plugins/data/common'; // @ts-ignore import { FieldFormat } from '../../../../field_formats/field_format'; // @ts-ignore @@ -28,12 +29,24 @@ import chrome from '../../../chrome'; // @ts-ignore import { fieldFormats } from '../../../registry/field_formats'; +interface TermsFieldFormatParams { + otherBucketLabel: string; + missingBucketLabel: string; + id: string; +} + +function isTermsFieldFormat( + serializedFieldFormat: SerializedFieldFormat +): serializedFieldFormat is SerializedFieldFormat { + return serializedFieldFormat.id === 'terms'; +} + const config = chrome.getUiSettingsClient(); const getConfig = (...args: any[]): any => config.get(...args); const getDefaultFieldFormat = () => ({ convert: identity }); -const getFieldFormat = (id: string, params: object) => { +const getFieldFormat = (id: string | undefined, params: object = {}) => { const Format = fieldFormats.byId[id]; if (Format) { return new Format(params, getConfig); @@ -42,7 +55,42 @@ const getFieldFormat = (id: string, params: object) => { } }; -export const getFormat = (mapping: any) => { +export type FieldFormat = any; + +export const createFormat = (agg: AggConfig): SerializedFieldFormat => { + const format: SerializedFieldFormat = agg.params.field ? agg.params.field.format.toJSON() : {}; + const formats: Record SerializedFieldFormat> = { + date_range: () => ({ id: 'string' }), + percentile_ranks: () => ({ id: 'percent' }), + count: () => ({ id: 'number' }), + cardinality: () => ({ id: 'number' }), + date_histogram: () => ({ + id: 'date', + params: { + pattern: agg.buckets.getScaledDateFormat(), + }, + }), + terms: () => ({ + id: 'terms', + params: { + id: format.id, + otherBucketLabel: agg.params.otherBucketLabel, + missingBucketLabel: agg.params.missingBucketLabel, + ...format.params, + }, + }), + range: () => ({ + id: 'range', + params: { id: format.id, ...format.params }, + }), + }; + + return formats[agg.type.name] ? formats[agg.type.name]() : format; +}; + +export type FormatFactory = (mapping?: SerializedFieldFormat) => FieldFormat; + +export const getFormat: FormatFactory = (mapping: SerializedFieldFormat = {}): FieldFormat => { if (!mapping) { return getDefaultFieldFormat(); } @@ -59,16 +107,17 @@ export const getFormat = (mapping: any) => { }); }); return new RangeFormat(); - } else if (id === 'terms') { + } else if (isTermsFieldFormat(mapping) && mapping.params) { + const params = mapping.params; return { getConverterFor: (type: string) => { - const format = getFieldFormat(mapping.params.id, mapping.params); + const format = getFieldFormat(params.id, mapping.params); return (val: string) => { if (val === '__other__') { - return mapping.params.otherBucketLabel; + return params.otherBucketLabel; } if (val === '__missing__') { - return mapping.params.missingBucketLabel; + return params.missingBucketLabel; } const parsedUrl = { origin: window.location.origin, @@ -79,12 +128,12 @@ export const getFormat = (mapping: any) => { }; }, convert: (val: string, type: string) => { - const format = getFieldFormat(mapping.params.id, mapping.params); + const format = getFieldFormat(params.id, mapping.params); if (val === '__other__') { - return mapping.params.otherBucketLabel; + return params.otherBucketLabel; } if (val === '__missing__') { - return mapping.params.missingBucketLabel; + return params.missingBucketLabel; } const parsedUrl = { origin: window.location.origin, diff --git a/src/plugins/data/common/expressions/expression_types/kibana_datatable.ts b/src/plugins/data/common/expressions/expression_types/kibana_datatable.ts index d5622ff50dd83..571e9ec7ff1e9 100644 --- a/src/plugins/data/common/expressions/expression_types/kibana_datatable.ts +++ b/src/plugins/data/common/expressions/expression_types/kibana_datatable.ts @@ -21,9 +21,15 @@ import { map } from 'lodash'; const name = 'kibana_datatable'; +export interface SerializedFieldFormat { + id?: string; + params?: TParams; +} + interface Column { id: string; name: string; + formatHint?: SerializedFieldFormat; } interface Row { diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index 076bb1c20ac10..0e53ee59761f5 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -8,9 +8,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable } from '@elastic/eui'; -import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { KibanaDatatable, LensMultiTable } from '../types'; +import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/types'; +import { LensMultiTable } from '../types'; import { RenderFunction } from '../interpreter_types'; +import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; export interface DatatableColumns { columnIds: string[]; @@ -106,7 +107,9 @@ export const datatableColumns: ExpressionFunction< }, }; -export const datatableRenderer: RenderFunction = { +export const getDatatableRenderer = ( + formatFactory: FormatFactory +): RenderFunction => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { defaultMessage: 'Datatable', @@ -115,12 +118,17 @@ export const datatableRenderer: RenderFunction = { validate: () => {}, reuseDomNode: true, render: async (domNode: Element, config: DatatableProps, _handlers: unknown) => { - ReactDOM.render(, domNode); + ReactDOM.render(, domNode); }, -}; +}); -function DatatableComponent(props: DatatableProps) { +function DatatableComponent(props: DatatableProps & { formatFactory: FormatFactory }) { const [firstTable] = Object.values(props.data.tables); + const formatters: Record> = {}; + + firstTable.columns.forEach(column => { + formatters[column.id] = props.formatFactory(column.formatHint); + }); return ( !!field)} - items={firstTable ? firstTable.rows : []} + items={ + firstTable + ? firstTable.rows.map(row => { + const formattedRow: Record = {}; + Object.entries(formatters).forEach(([columnId, formatter]) => { + formattedRow[columnId] = formatter.convert(row[columnId]); + }); + return formattedRow; + }) + : [] + } /> ); } diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx index 7bcddae13e1ac..52f4f99513e7a 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx @@ -6,6 +6,7 @@ // import { Registry } from '@kbn/interpreter/target/common'; import { CoreSetup } from 'src/core/public'; +import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { datatableVisualization } from './visualization'; import { @@ -13,19 +14,29 @@ import { functionsRegistry, } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { InterpreterSetup, RenderFunction } from '../interpreter_types'; -import { datatable, datatableColumns, datatableRenderer } from './expression'; +import { datatable, datatableColumns, getDatatableRenderer } from './expression'; export interface DatatableVisualizationPluginSetupPlugins { interpreter: InterpreterSetup; + // TODO this is a simulated NP plugin. + // Once field formatters are actually migrated, the actual shim can be used + fieldFormat: { + formatFactory: FormatFactory; + }; } class DatatableVisualizationPlugin { constructor() {} - setup(_core: CoreSetup | null, { interpreter }: DatatableVisualizationPluginSetupPlugins) { + setup( + _core: CoreSetup | null, + { interpreter, fieldFormat }: DatatableVisualizationPluginSetupPlugins + ) { interpreter.functionsRegistry.register(() => datatableColumns); interpreter.functionsRegistry.register(() => datatable); - interpreter.renderersRegistry.register(() => datatableRenderer as RenderFunction); + interpreter.renderersRegistry.register( + () => getDatatableRenderer(fieldFormat.formatFactory) as RenderFunction + ); return datatableVisualization; } @@ -41,5 +52,8 @@ export const datatableVisualizationSetup = () => renderersRegistry, functionsRegistry, }, + fieldFormat: { + formatFactory: getFormat, + }, }); export const datatableVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index c7747ace106fd..2b7e35876bb63 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { LensMultiTable, KibanaDatatable } from '../types'; +import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/types'; +import { LensMultiTable } from '../types'; interface MergeTables { layerIds: string[]; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts index 1fe57f42fc987..854284f98a8d0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts @@ -5,8 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { KibanaDatatable } from '../types'; +import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/types'; interface FilterRatioKey { id: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index b7d761b42030a..2ab26455a9f7a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -243,6 +243,7 @@ describe('IndexPattern Data Source', () => { index=\\"1\\" metricsAtAllLevels=false partialRows=false + includeFormatHints=true aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":1,\\"extended_bounds\\":{}}}]' | lens_rename_columns idMap='{\\"col-0-col1\\":\\"col1\\",\\"col-1-col2\\":\\"col2\\"}'" `); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts index 01361ada9bf02..5a508bf2a84f6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts @@ -5,8 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { KibanaDatatable } from '../types'; +import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/types'; interface RemapArgs { idMap: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 46095cd434335..71e8473aa6839 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -74,6 +74,7 @@ function getExpressionForLayer( index="${indexPattern.id}" metricsAtAllLevels=false partialRows=false + includeFormatHints=true aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 77f592a67a5fd..6deab82c5658e 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -6,7 +6,7 @@ import { Ast } from '@kbn/interpreter/common'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { Query } from 'src/plugins/data/common'; +import { Query, KibanaDatatable } from 'src/plugins/data/common'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; @@ -167,14 +167,6 @@ export interface LensMultiTable { tables: Record; } -// This is a temporary type definition, to be replaced with -// the official Kibana Datatable type definition. -export interface KibanaDatatable { - type: 'kibana_datatable'; - rows: Array>; - columns: Array<{ id: string; name: string }>; -} - export interface VisualizationProps { dragDropContext: DragContextState; frame: FramePublicAPI; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index 71fb202a5cf3c..deaf7254d6789 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -15,12 +15,14 @@ exports[`xy_expression XYChart component it renders area 1`] = ` id="x" position="bottom" showGridLines={false} + tickFormat={[Function]} title="" /> -`; \ No newline at end of file +`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx index b16c26f6dee45..81098b0a420b2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx @@ -5,6 +5,7 @@ */ import { CoreSetup } from 'src/core/public'; +import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { xyVisualization } from './xy_visualization'; import { @@ -12,23 +13,30 @@ import { functionsRegistry, } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { InterpreterSetup, RenderFunction } from '../interpreter_types'; -import { xyChart, xyChartRenderer } from './xy_expression'; +import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; export interface XyVisualizationPluginSetupPlugins { interpreter: InterpreterSetup; + // TODO this is a simulated NP plugin. + // Once field formatters are actually migrated, the actual shim can be used + fieldFormat: { + formatFactory: FormatFactory; + }; } class XyVisualizationPlugin { constructor() {} - setup(_core: CoreSetup | null, { interpreter }: XyVisualizationPluginSetupPlugins) { + setup(_core: CoreSetup | null, { interpreter, fieldFormat }: XyVisualizationPluginSetupPlugins) { interpreter.functionsRegistry.register(() => legendConfig); interpreter.functionsRegistry.register(() => xConfig); interpreter.functionsRegistry.register(() => layerConfig); interpreter.functionsRegistry.register(() => xyChart); - interpreter.renderersRegistry.register(() => xyChartRenderer as RenderFunction); + interpreter.renderersRegistry.register( + () => getXyChartRenderer(fieldFormat.formatFactory) as RenderFunction + ); return xyVisualization; } @@ -44,5 +52,8 @@ export const xyVisualizationSetup = () => renderersRegistry, functionsRegistry, }, + fieldFormat: { + formatFactory: getFormat, + }, }); export const xyVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index b21b3229efd57..a3d8a3b3f0133 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Axis } from '@elastic/charts'; import { AreaSeries, BarSeries, Position, LineSeries, Settings, ScaleType } from '@elastic/charts'; import { xyChart, XYChart } from './xy_expression'; import { LensMultiTable } from '../types'; @@ -17,8 +18,16 @@ function sampleArgs() { tables: { first: { type: 'kibana_datatable', - columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], - rows: [{ a: 1, b: 2, c: 3 }, { a: 1, b: 5, c: 4 }], + columns: [ + { + id: 'a', + name: 'a', + formatHint: { id: 'number', params: { pattern: '0,0.000' } }, + }, + { id: 'b', name: 'b', formatHint: { id: 'number', params: { pattern: '000,0' } } }, + { id: 'c', name: 'c', formatHint: { id: 'string' } }, + ], + rows: [{ a: 1, b: 2, c: 'I' }, { a: 1, b: 5, c: 'J' }], }, }, }; @@ -91,6 +100,15 @@ describe('xy_expression', () => { }); describe('XYChart component', () => { + let getFormatSpy: jest.Mock; + let convertSpy: jest.Mock; + + beforeEach(() => { + convertSpy = jest.fn(x => x); + getFormatSpy = jest.fn(); + getFormatSpy.mockReturnValue({ convert: convertSpy }); + }); + test('it renders line', () => { const { data, args } = sampleArgs(); @@ -98,6 +116,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -110,6 +129,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -122,6 +142,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -134,6 +155,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -147,6 +169,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -160,6 +183,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -177,6 +201,7 @@ describe('xy_expression', () => { isHorizontal: true, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }], }} + formatFactory={getFormatSpy} /> ); expect(component).toMatchSnapshot(); @@ -188,28 +213,28 @@ describe('xy_expression', () => { test('it rewrites the rows based on provided labels', () => { const { data, args } = sampleArgs(); - const component = shallow(); + const component = shallow(); expect(component.find(LineSeries).prop('data')).toEqual([ - { 'Label A': 1, 'Label B': 2, c: 3 }, - { 'Label A': 1, 'Label B': 5, c: 4 }, + { 'Label A': 1, 'Label B': 2, c: 'I' }, + { 'Label A': 1, 'Label B': 5, c: 'J' }, ]); }); test('it uses labels as Y accessors', () => { const { data, args } = sampleArgs(); - const component = shallow(); + const component = shallow(); expect(component.find(LineSeries).prop('yAccessors')).toEqual(['Label A', 'Label B']); }); - test('it indicates a linear scale for a numeric X axis', () => { + test('it indicates an ordinal scale for a string X axis', () => { const { data, args } = sampleArgs(); - const component = shallow(); - expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Linear); + const component = shallow(); + expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); }); - test('it indicates an ordinal scale for a string X axis', () => { + test('it indicates a linear scale for a numeric X axis', () => { const { args } = sampleArgs(); const data: LensMultiTable = { @@ -218,13 +243,62 @@ describe('xy_expression', () => { first: { type: 'kibana_datatable', columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], - rows: [{ a: 1, b: 2, c: 'Hello' }, { a: 6, b: 5, c: 'World' }], + rows: [{ a: 1, b: 2, c: 3 }, { a: 6, b: 5, c: 9 }], }, }, }; - const component = shallow(); - expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); + const component = shallow(); + expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Linear); + }); + + test('it gets the formatter for the x axis', () => { + const { data, args } = sampleArgs(); + + shallow(); + + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); + }); + + test('it gets a default formatter for y if there are multiple y accessors', () => { + const { data, args } = sampleArgs(); + + shallow(); + + expect(getFormatSpy).toHaveBeenCalledTimes(2); + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'number' }); + }); + + test('it gets the formatter for the y axis if there is only one accessor', () => { + const { data, args } = sampleArgs(); + + shallow( + + ); + expect(getFormatSpy).toHaveBeenCalledWith({ + id: 'number', + params: { pattern: '0,0.000' }, + }); + }); + + test('it should pass the formatter function to the axis', () => { + const { data, args } = sampleArgs(); + + const instance = shallow( + + ); + + const tickFormatter = instance + .find(Axis) + .first() + .prop('tickFormat'); + tickFormatter('I'); + + expect(convertSpy).toHaveBeenCalledWith('I'); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 9d7fffc17c086..37680bbe8e3d9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -22,6 +22,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, IconType } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { RenderFunction } from '../interpreter_types'; @@ -85,7 +86,7 @@ export interface XYChartProps { args: XYArgs; } -export const xyChartRenderer: RenderFunction = { +export const getXyChartRenderer = (formatFactory: FormatFactory): RenderFunction => ({ name: 'lens_xy_chart_renderer', displayName: 'XY Chart', help: 'X/Y Chart Renderer', @@ -94,18 +95,24 @@ export const xyChartRenderer: RenderFunction = { render: async (domNode: Element, config: XYChartProps, _handlers: unknown) => { ReactDOM.render( - + , domNode ); }, -}; +}); function getIconForSeriesType(seriesType: SeriesType): IconType { return visualizationTypes.find(c => c.id === seriesType)!.icon || 'empty'; } -export function XYChart({ data, args }: XYChartProps) { +export function XYChart({ + data, + args, + formatFactory, +}: XYChartProps & { + formatFactory: FormatFactory; +}) { const { legend, layers, isHorizontal } = args; if (Object.values(data.tables).every(table => table.rows.length === 0)) { @@ -127,6 +134,23 @@ export function XYChart({ data, args }: XYChartProps) { ); } + // use formatting hint of first x axis column to format ticks + const xAxisColumn = Object.values(data.tables)[0].columns.find( + ({ id }) => id === layers[0].xAccessor + ); + const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); + + // use default number formatter for y axis and use formatting hint if there is just a single y column + let yAxisFormatter = formatFactory({ id: 'number' }); + if (layers.length === 1 && layers[0].accessors.length === 1) { + const firstYAxisColumn = Object.values(data.tables)[0].columns.find( + ({ id }) => id === layers[0].accessors[0] + ); + if (firstYAxisColumn && firstYAxisColumn.formatHint) { + yAxisFormatter = formatFactory(firstYAxisColumn.formatHint); + } + } + return ( xAxisFormatter.convert(d)} /> yAxisFormatter.convert(d)} /> {layers.map(