diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
index ba235a20b97ce..21d7de9f1d880 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
@@ -11,13 +11,16 @@ import React, { Component } from 'react';
import { parse as parseUrl } from 'url';
import PropTypes from 'prop-types';
import { RedirectAppLinks } from '../../../../../../kibana_react/public';
+import { getMetricsField } from '../../lib/get_metrics_field';
import { createTickFormatter } from '../../lib/tick_formatter';
+import { createFieldFormatter } from '../../lib/create_field_formatter';
import { isSortable } from './is_sortable';
import { EuiToolTip, EuiIcon } from '@elastic/eui';
import { replaceVars } from '../../lib/replace_vars';
import { FIELD_FORMAT_IDS } from '../../../../../../../plugins/field_formats/common';
import { FormattedMessage } from '@kbn/i18n/react';
import { getFieldFormats, getCoreStart } from '../../../../services';
+import { DATA_FORMATTERS } from '../../../../../common/enums';
import { getValueOrEmpty } from '../../../../../common/empty_label';
function getColor(rules, colorKey, value) {
@@ -57,26 +60,40 @@ class TableVis extends Component {
}
renderRow = (row) => {
- const { model } = this.props;
+ const { model, fieldFormatMap, getConfig } = this.props;
let rowDisplay = getValueOrEmpty(
model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key
);
+ // we should skip url field formatting for key if tsvb have drilldown_url
+ if (fieldFormatMap?.[model.pivot_id]?.id !== FIELD_FORMAT_IDS.URL || !model.drilldown_url) {
+ const formatter = createFieldFormatter(model?.pivot_id, fieldFormatMap, 'html');
+ rowDisplay = ; // eslint-disable-line react/no-danger
+ }
+
if (model.drilldown_url) {
const url = replaceVars(model.drilldown_url, {}, { key: row.key });
rowDisplay = {rowDisplay};
}
+
const columns = row.series
.filter((item) => item)
.map((item) => {
const column = this.visibleSeries.find((c) => c.id === item.id);
if (!column) return null;
- const formatter = createTickFormatter(
- column.formatter,
- column.value_template,
- this.props.getConfig
+ const hasColorRules = column.color_rules?.some(
+ ({ value, operator, text }) => value || operator || text
);
+ const formatter =
+ column.formatter === DATA_FORMATTERS.DEFAULT
+ ? createFieldFormatter(
+ getMetricsField(column.metrics),
+ fieldFormatMap,
+ 'html',
+ hasColorRules
+ )
+ : createTickFormatter(column.formatter, column.value_template, getConfig);
const value = formatter(item.last);
let trend;
if (column.trend_arrows) {
@@ -95,7 +112,8 @@ class TableVis extends Component {
className="eui-textRight"
style={style}
>
- {value}
+ {/* eslint-disable-next-line react/no-danger */}
+
{trend}
);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js
index 01ba8b6e28114..4257c35a6d4c2 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js
@@ -8,7 +8,9 @@
import { i18n } from '@kbn/i18n';
import PropTypes from 'prop-types';
-import React, { useState, useEffect } from 'react';
+import { last } from 'lodash';
+import React, { useMemo, useState, useEffect, useCallback } from 'react';
+import { DATA_FORMATTERS } from '../../../../../common/enums';
import { DataFormatPicker } from '../../data_format_picker';
import { createSelectHandler } from '../../lib/create_select_handler';
import { YesNo } from '../../yes_no';
@@ -29,6 +31,7 @@ import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { SeriesConfigQueryBarWithIgnoreGlobalFilter } from '../../series_config_query_bar_with_ignore_global_filter';
import { PalettePicker } from '../../palette_picker';
import { getCharts } from '../../../../services';
+import { checkIfNumericMetric } from '../../lib/check_if_numeric_metric';
import { isPercentDisabled } from '../../lib/stacked';
import { STACKED_OPTIONS } from '../../../visualizations/constants/chart';
@@ -328,6 +331,13 @@ export const TimeseriesConfig = injectI18n(function (props) {
? props.model.series_index_pattern
: props.indexPatternForQuery;
+ const changeModelFormatter = useCallback((formatter) => props.onChange({ formatter }), [props]);
+ const isNumericMetric = useMemo(
+ () => checkIfNumericMetric(last(model.metrics), props.fields, seriesIndexPattern),
+ [model.metrics, props.fields, seriesIndexPattern]
+ );
+ const isKibanaIndexPattern = props.panel.use_kibana_indexes || seriesIndexPattern === '';
+
const initialPalette = model.palette ?? {
type: 'palette',
name: 'default',
@@ -344,10 +354,13 @@ export const TimeseriesConfig = injectI18n(function (props) {
return (
-
-
-
-
+
+
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
index d9440804701b2..fed295fef9d30 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
@@ -13,7 +13,10 @@ import { startsWith, get, cloneDeep, map } from 'lodash';
import { htmlIdGenerator } from '@elastic/eui';
import { ScaleType } from '@elastic/charts';
+import { getMetricsField } from '../../lib/get_metrics_field';
import { createTickFormatter } from '../../lib/tick_formatter';
+import { createFieldFormatter } from '../../lib/create_field_formatter';
+import { checkIfSeriesHaveSameFormatters } from '../../lib/check_if_series_have_same_formatters';
import { TimeSeries } from '../../../visualizations/views/timeseries';
import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public';
import { replaceVars } from '../../lib/replace_vars';
@@ -21,6 +24,7 @@ import { getInterval } from '../../lib/get_interval';
import { createIntervalBasedFormatter } from '../../lib/create_interval_based_formatter';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
import { getCoreStart } from '../../../../services';
+import { DATA_FORMATTERS } from '../../../../../common/enums';
class TimeseriesVisualization extends Component {
static propTypes = {
@@ -51,6 +55,16 @@ class TimeseriesVisualization extends Component {
};
applyDocTo = (template) => (doc) => {
+ const { fieldFormatMap } = this.props;
+
+ // formatting each doc value with custom field formatter if fieldFormatMap contains that doc field name
+ Object.keys(doc).forEach((fieldName) => {
+ if (fieldFormatMap?.[fieldName]) {
+ const valueFieldFormatter = createFieldFormatter(fieldName, fieldFormatMap);
+ doc[fieldName] = valueFieldFormatter(doc[fieldName]);
+ }
+ });
+
const vars = replaceVars(template, null, doc, {
noEscape: true,
});
@@ -139,7 +153,16 @@ class TimeseriesVisualization extends Component {
};
render() {
- const { model, visData, onBrush, onFilterClick, syncColors, palettesService } = this.props;
+ const {
+ model,
+ visData,
+ onBrush,
+ onFilterClick,
+ syncColors,
+ palettesService,
+ fieldFormatMap,
+ getConfig,
+ } = this.props;
const series = get(visData, `${model.id}.series`, []);
const interval = getInterval(visData, model);
const yAxisIdGenerator = htmlIdGenerator('yaxis');
@@ -152,10 +175,6 @@ class TimeseriesVisualization extends Component {
const yAxis = [];
let mainDomainAdded = false;
- const allSeriesHaveSameFormatters = seriesModel.every(
- (seriesGroup) => seriesGroup.formatter === seriesModel[0].formatter
- );
-
this.showToastNotification = null;
seriesModel.forEach((seriesGroup) => {
@@ -166,10 +185,12 @@ class TimeseriesVisualization extends Component {
? TimeseriesVisualization.getYAxisDomain(seriesGroup)
: undefined;
const isCustomDomain = groupId !== mainAxisGroupId;
- const seriesGroupTickFormatter = TimeseriesVisualization.getTickFormatter(
- seriesGroup,
- this.props.getConfig
- );
+
+ const seriesGroupTickFormatter =
+ seriesGroup.formatter === DATA_FORMATTERS.DEFAULT
+ ? createFieldFormatter(getMetricsField(seriesGroup.metrics), fieldFormatMap)
+ : TimeseriesVisualization.getTickFormatter(seriesGroup, getConfig);
+
const palette = {
...seriesGroup.palette,
name:
@@ -214,8 +235,12 @@ class TimeseriesVisualization extends Component {
: seriesGroupTickFormatter,
});
} else if (!mainDomainAdded) {
+ const tickFormatter = checkIfSeriesHaveSameFormatters(seriesModel, fieldFormatMap)
+ ? seriesGroupTickFormatter
+ : (val) => val;
+
TimeseriesVisualization.addYAxis(yAxis, {
- tickFormatter: allSeriesHaveSameFormatters ? seriesGroupTickFormatter : (val) => val,
+ tickFormatter,
id: yAxisIdGenerator('main'),
groupId: mainAxisGroupId,
position: model.axis_position,
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js
index fd155623d5da7..d6e7484e903bf 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js
@@ -11,9 +11,15 @@ import { shallow } from 'enzyme';
import { TimeSeries } from '../../../visualizations/views/timeseries';
import TimeseriesVisualization from './vis';
import { setFieldFormats } from '../../../../services';
+import { createFieldFormatter } from '../../lib/create_field_formatter';
import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common';
+import { METRIC_TYPES } from '../../../../../../data/common';
import { getFieldFormatsRegistry } from '../../../../../../data/public/test_utils';
+jest.mock('../../../../../../data/public/services', () => ({
+ getUiSettings: () => ({ get: jest.fn() }),
+}));
+
describe('TimeseriesVisualization', () => {
describe('TimeSeries Y-Axis formatted value', () => {
const config = {
@@ -29,19 +35,34 @@ describe('TimeseriesVisualization', () => {
})
);
- const setupTimeSeriesPropsWithFormatters = (...formatters) => {
- const series = formatters.map((formatter) => ({
- id,
+ const setupTimeSeriesProps = (formatters, valueTemplates) => {
+ const series = formatters.map((formatter, index) => ({
+ id: id + index,
formatter,
+ value_template: valueTemplates?.[index],
data: [],
+ metrics: [
+ {
+ type: METRIC_TYPES.AVG,
+ field: `field${index}`,
+ },
+ ],
}));
+ const fieldFormatMap = {
+ field0: { id: 'duration', params: { inputFormat: 'years' } },
+ field1: { id: 'duration', params: { inputFormat: 'years' } },
+ field2: { id: 'duration', params: { inputFormat: 'months' } },
+ field3: { id: 'number', params: { pattern: '$0,0.[00]' } },
+ };
+
const timeSeriesVisualization = shallow(
config[key]}
model={{
id,
series,
+ use_kibana_indexes: true,
}}
visData={{
[id]: {
@@ -49,56 +70,69 @@ describe('TimeseriesVisualization', () => {
series,
},
}}
+ fieldFormatMap={fieldFormatMap}
+ createCustomFieldFormatter={createFieldFormatter}
/>
);
return timeSeriesVisualization.find(TimeSeries).props();
};
- test('should be byte for single byte series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte');
+ test('should return byte formatted value from yAxis formatter for single byte series', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['byte']);
const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value);
expect(yAxisFormattedValue).toBe('500B');
});
- test('should have custom format for single series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('0.00bitd');
+ test('should return custom formatted value from yAxis formatter for single series with custom formatter', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['0.00bitd']);
const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value);
expect(yAxisFormattedValue).toBe('500.00bit');
});
- test('should be the same number for byte and percent series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'percent');
+ test('should return the same number from yAxis formatter for byte and percent series', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['byte', 'percent']);
const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value);
expect(yAxisFormattedValue).toBe(value);
});
- test('should be the same stringified number for byte and percent series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'percent');
+ test('should return the same stringified number from yAxis formatter for byte and percent series', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['byte', 'percent']);
const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value.toString());
expect(yAxisFormattedValue).toBe('500');
});
- test('should be byte for two byte formatted series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'byte');
+ test('should return byte formatted value from yAxis formatter and from two byte formatted series with the same value templates', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['byte', 'byte']);
+ const { series, yAxis } = timeSeriesProps;
- const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value);
- const firstSeriesFormattedValue = timeSeriesProps.series[0].tickFormat(value);
+ expect(series[0].tickFormat(value)).toBe('500B');
+ expect(series[1].tickFormat(value)).toBe('500B');
+ expect(yAxis[0].tickFormatter(value)).toBe('500B');
+ });
- expect(firstSeriesFormattedValue).toBe('500B');
- expect(yAxisFormattedValue).toBe(firstSeriesFormattedValue);
+ test('should return simple number from yAxis formatter and different values from the same byte formatters, but with different value templates', () => {
+ const timeSeriesProps = setupTimeSeriesProps(
+ ['byte', 'byte'],
+ ['{{value}}', '{{value}} value']
+ );
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500B');
+ expect(series[1].tickFormat(value)).toBe('500B value');
+ expect(yAxis[0].tickFormatter(value)).toBe(value);
});
- test('should be percent for three percent formatted series', () => {
- const timeSeriesProps = setupTimeSeriesPropsWithFormatters('percent', 'percent', 'percent');
+ test('should return percent formatted value from yAxis formatter and three percent formatted series with the same value templates', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['percent', 'percent', 'percent']);
const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value);
const firstSeriesFormattedValue = timeSeriesProps.series[0].tickFormat(value);
@@ -106,5 +140,56 @@ describe('TimeseriesVisualization', () => {
expect(firstSeriesFormattedValue).toBe('50000%');
expect(yAxisFormattedValue).toBe(firstSeriesFormattedValue);
});
+
+ test('should return simple number from yAxis formatter and different values for the same value templates, but with different formatters', () => {
+ const timeSeriesProps = setupTimeSeriesProps(
+ ['number', 'byte'],
+ ['{{value}} template', '{{value}} template']
+ );
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500 template');
+ expect(series[1].tickFormat(value)).toBe('500B template');
+ expect(yAxis[0].tickFormatter(value)).toBe(value);
+ });
+
+ test('should return field formatted value for yAxis and single series with default formatter', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['default']);
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500 years');
+ expect(yAxis[0].tickFormatter(value)).toBe('500 years');
+ });
+
+ test('should return custom field formatted value for yAxis and both series having same fieldFormats', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['default', 'default']);
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500 years');
+ expect(series[1].tickFormat(value)).toBe('500 years');
+ expect(yAxis[0].tickFormatter(value)).toBe('500 years');
+ });
+
+ test('should return simple number from yAxis formatter and default formatted values for series', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['default', 'default', 'default', 'default']);
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500 years');
+ expect(series[1].tickFormat(value)).toBe('500 years');
+ expect(series[2].tickFormat(value)).toBe('42 years');
+ expect(series[3].tickFormat(value)).toBe('$500');
+ expect(yAxis[0].tickFormatter(value)).toBe(value);
+ });
+
+ test('should return simple number from yAxis formatter and correctly formatted series values', () => {
+ const timeSeriesProps = setupTimeSeriesProps(['default', 'byte', 'percent', 'default']);
+ const { series, yAxis } = timeSeriesProps;
+
+ expect(series[0].tickFormat(value)).toBe('500 years');
+ expect(series[1].tickFormat(value)).toBe('500B');
+ expect(series[2].tickFormat(value)).toBe('50000%');
+ expect(series[3].tickFormat(value)).toBe('$500');
+ expect(yAxis[0].tickFormatter(value)).toBe(value);
+ });
});
});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
index 0b3a24615c0e3..8176f6ece2805 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
@@ -7,7 +7,9 @@
*/
import { getCoreStart } from '../../../../services';
+import { getMetricsField } from '../../lib/get_metrics_field';
import { createTickFormatter } from '../../lib/tick_formatter';
+import { createFieldFormatter } from '../../lib/create_field_formatter';
import { TopN } from '../../../visualizations/views/top_n';
import { getLastValue } from '../../../../../common/last_value_utils';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
@@ -15,6 +17,7 @@ import { replaceVars } from '../../lib/replace_vars';
import PropTypes from 'prop-types';
import React from 'react';
import { sortBy, first, get } from 'lodash';
+import { DATA_FORMATTERS } from '../../../../../common/enums';
import { getOperator, shouldOperate } from '../../../../../common/operators_utils';
function sortByDirection(data, direction, fn) {
@@ -38,17 +41,17 @@ function sortSeries(visData, model) {
}
function TopNVisualization(props) {
- const { backgroundColor, model, visData } = props;
+ const { backgroundColor, model, visData, fieldFormatMap, getConfig } = props;
const series = sortSeries(visData, model).map((item) => {
const id = first(item.id.split(/:/));
const seriesConfig = model.series.find((s) => s.id === id);
if (seriesConfig) {
- const tickFormatter = createTickFormatter(
- seriesConfig.formatter,
- seriesConfig.value_template,
- props.getConfig
- );
+ const tickFormatter =
+ seriesConfig.formatter === DATA_FORMATTERS.DEFAULT
+ ? createFieldFormatter(getMetricsField(seriesConfig.metrics), fieldFormatMap, 'html')
+ : createTickFormatter(seriesConfig.formatter, seriesConfig.value_template, getConfig);
+
const value = getLastValue(item.data);
let color = item.color || seriesConfig.color;
if (model.bar_color_rules) {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
index 945a7ac986d3e..86c0af1c97980 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
@@ -15,7 +15,7 @@ import { getSplitByTermsColor } from '../lib/get_split_by_terms_color';
export function visWithSplits(WrappedComponent) {
function SplitVisComponent(props) {
- const { model, visData, syncColors, palettesService } = props;
+ const { model, visData, syncColors, palettesService, fieldFormatMap } = props;
const getSeriesColor = useCallback(
(seriesName, seriesId, baseColor) => {
@@ -34,10 +34,11 @@ export function visWithSplits(WrappedComponent) {
seriesPalette: palette,
palettesRegistry: palettesService,
syncColors,
+ fieldFormatMap,
};
return getSplitByTermsColor(props) || null;
},
- [model, palettesService, syncColors, visData]
+ [fieldFormatMap, model.id, model.series, palettesService, syncColors, visData]
);
if (!model || !visData || !visData[model.id] || visData[model.id].series.length === 1)
@@ -114,6 +115,7 @@ export function visWithSplits(WrappedComponent) {
additionalLabel={getValueOrEmpty(additionalLabel)}
backgroundColor={props.backgroundColor}
getConfig={props.getConfig}
+ fieldFormatMap={props.fieldFormatMap}
/>
);
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss
index 7f3c049a131d2..fdab7f02957e0 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss
@@ -47,6 +47,8 @@
font-size: .9em; /* 1 */
line-height: 1em; /* 1 */
text-align: center;
+ // make gauge value the target for pointer-events
+ pointer-events: all;
.tvbVisGauge--reversed & {
color: $tvbValueColorReversed;
@@ -71,4 +73,6 @@
display: flex;
flex-direction: column;
flex: 1 0 auto;
+ // disable gauge container pointer-events as it shouldn't be event target
+ pointer-events: none;
}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
index 723a054baeeae..ca5021a882932 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
@@ -117,7 +117,8 @@ export class Gauge extends Component {
ref="label"
data-test-subj="gaugeValue"
>
- {formatter(value)}
+ {/* eslint-disable-next-line react/no-danger */}
+