[Gauge] Vis editors gauge legacy percent mode. (#126318)

* Added legacy percentage mode to the gauge.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yaroslav Kuznietsov 2022-03-08 11:09:03 +02:00 committed by GitHub
parent 5ad355e8c7
commit 720fbed521
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 170 additions and 60 deletions

View file

@ -47,6 +47,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "arc",
"ticksPosition": "auto",
},
@ -96,6 +97,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "arc",
"ticksPosition": "auto",
},
@ -143,6 +145,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -190,6 +193,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -237,6 +241,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "bands",
},
@ -286,6 +291,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "circle",
"ticksPosition": "auto",
},
@ -335,6 +341,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "circle",
"ticksPosition": "auto",
},
@ -382,6 +389,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -429,6 +437,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "hidden",
},
@ -476,6 +485,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -523,6 +533,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -570,6 +581,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -617,6 +629,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
@ -664,6 +677,7 @@ Object {
"metric": "col-0-1",
"min": "col-1-2",
"palette": undefined,
"percentageMode": false,
"shape": "verticalBullet",
"ticksPosition": "auto",
},

View file

@ -180,6 +180,14 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({
defaultMessage: 'Specifies the mode of centralMajor',
}),
},
// used only in legacy gauge, consider it as @deprecated
percentageMode: {
types: ['boolean'],
default: false,
help: i18n.translate('expressionGauge.functions.gauge.percentageMode.help', {
defaultMessage: 'Enables relative precentage mode',
}),
},
ariaLabel: {
types: ['string'],
help: i18n.translate('expressionGauge.functions.gaugeChart.config.ariaLabel.help', {

View file

@ -45,6 +45,8 @@ export interface GaugeState {
colorMode?: GaugeColorMode;
palette?: PaletteOutput<CustomPaletteParams>;
shape: GaugeShape;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
percentageMode?: boolean;
}
export type GaugeArguments = GaugeState & {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { ChartsPluginSetup } from '../../../../charts/public';
import type { ChartsPluginSetup, PaletteRegistry } from '../../../../charts/public';
import type { IFieldFormat, SerializedFieldFormat } from '../../../../field_formats/common';
import type { GaugeExpressionProps } from './expression_functions';
@ -15,6 +15,7 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
export type GaugeRenderProps = GaugeExpressionProps & {
formatFactory: FormatFactory;
chartsThemeService: ChartsPluginSetup['theme'];
paletteService: PaletteRegistry;
};
export interface ColorStop {

View file

@ -41,6 +41,7 @@ exports[`GaugeComponent renders the chart 1`] = `
4,
]
}
tooltipValueFormatter={[Function]}
/>
</Chart>
`;

View file

@ -54,6 +54,7 @@ jest.mock('@elastic/charts', () => {
});
const chartsThemeService = chartPluginMock.createSetupContract().theme;
const paletteThemeService = chartPluginMock.createSetupContract().palettes;
const formatService = fieldFormatsServiceMock.createStartContract();
const args: GaugeArguments = {
labelMajor: 'Gauge',
@ -81,12 +82,13 @@ const createData = (
describe('GaugeComponent', function () {
let wrapperProps: GaugeRenderProps;
beforeAll(() => {
beforeAll(async () => {
wrapperProps = {
data: createData(),
chartsThemeService,
args,
formatFactory: formatService.deserialize,
paletteService: await paletteThemeService.getPalettes(),
};
});

View file

@ -5,10 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, memo } from 'react';
import React, { FC, memo, useCallback } from 'react';
import { Chart, Goal, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CustomPaletteState } from '../../../../charts/public';
import type { CustomPaletteState, PaletteOutput } from '../../../../charts/public';
import { EmptyPlaceholder } from '../../../../charts/public';
import { isVisDimension } from '../../../../visualizations/common/utils';
import {
@ -42,37 +42,7 @@ declare global {
}
}
function normalizeColors(
{ colors, stops, range, rangeMin, rangeMax }: CustomPaletteState,
min: number,
max: number
) {
if (!colors) {
return;
}
const colorsOutOfRangeSmaller = Math.max(
stops.filter((stop, i) => (range === 'percent' ? stop < 0 : stop < min)).length,
0
);
let updatedColors = colors.slice(colorsOutOfRangeSmaller);
let correctMin = rangeMin;
let correctMax = rangeMax;
if (range === 'percent') {
correctMin = min + rangeMin * ((max - min) / 100);
correctMax = min + rangeMax * ((max - min) / 100);
}
if (correctMin > min && isFinite(correctMin)) {
updatedColors = [`rgba(255,255,255,0)`, ...updatedColors];
}
if (correctMax < max && isFinite(correctMax)) {
updatedColors = [...updatedColors, `rgba(255,255,255,0)`];
}
return updatedColors;
}
const TRANSPARENT = `rgba(255,255,255,0)`;
function normalizeBands(
{ colors, stops, range, rangeMax, rangeMin }: CustomPaletteState,
@ -111,6 +81,28 @@ function normalizeBands(
return [...firstRanges, ...orderedStops, ...lastRanges];
}
const toPercents = (min: number, max: number) => (v: number) => (v - min) / (max - min);
function normalizeBandsLegacy({ colors, stops }: CustomPaletteState, value: number) {
const min = stops[0];
const max = stops[stops.length - 1];
const convertToPercents = toPercents(min, max);
const normalizedStops = stops.map(convertToPercents);
if (max < value) {
normalizedStops.push(convertToPercents(value));
}
return normalizedStops;
}
function actualValueToPercentsLegacy({ stops }: CustomPaletteState, value: number) {
const min = stops[0];
const max = stops[stops.length - 1];
const convertToPercents = toPercents(min, max);
return convertToPercents(value);
}
function getTitle(
majorMode?: GaugeLabelMajorMode | GaugeCentralMajorMode,
major?: string,
@ -144,7 +136,8 @@ function getTicksLabels(baseStops: number[]) {
function getTicks(
ticksPosition: GaugeTicksPosition,
range: [number, number],
colorBands?: number[]
colorBands?: number[],
percentageMode?: boolean
) {
if (ticksPosition === GaugeTicksPositions.HIDDEN) {
return [];
@ -158,16 +151,40 @@ function getTicks(
const min = Math.min(...(colorBands || []), ...range);
const max = Math.max(...(colorBands || []), ...range);
const step = (max - min) / TICKS_NO;
return [
const ticks = [
...Array(TICKS_NO)
.fill(null)
.map((_, i) => Number((min + step * i).toFixed(2))),
max,
];
const convertToPercents = toPercents(min, max);
return percentageMode ? ticks.map(convertToPercents) : ticks;
}
const calculateRealRangeValueMin = (
relativeRangeValue: number,
{ min, max }: { min: number; max: number }
) => {
if (isFinite(relativeRangeValue)) {
return relativeRangeValue * ((max - min) / 100);
}
return min;
};
const calculateRealRangeValueMax = (
relativeRangeValue: number,
{ min, max }: { min: number; max: number }
) => {
if (isFinite(relativeRangeValue)) {
return relativeRangeValue * ((max - min) / 100);
}
return max;
};
export const GaugeComponent: FC<GaugeRenderProps> = memo(
({ data, args, formatFactory, chartsThemeService }) => {
({ data, args, formatFactory, paletteService, chartsThemeService }) => {
const {
shape: gaugeType,
palette,
@ -179,6 +196,40 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
centralMajorMode,
ticksPosition,
} = args;
const getColor = useCallback(
(
value,
paletteConfig: PaletteOutput<CustomPaletteState>,
bands: number[],
percentageMode?: boolean
) => {
const { rangeMin, rangeMax, range }: CustomPaletteState = paletteConfig.params!;
const minRealValue = bands[0];
const maxRealValue = bands[bands.length - 1];
let min = rangeMin;
let max = rangeMax;
let stops = paletteConfig.params?.stops ?? [];
if (percentageMode) {
stops = bands.map((v) => v * 100);
}
if (range === 'percent') {
const minMax = { min: minRealValue, max: maxRealValue };
min = calculateRealRangeValueMin(min, minMax);
max = calculateRealRangeValueMax(max, minMax);
}
return paletteService
.get(paletteConfig?.name ?? 'custom')
.getColorForValue?.(value, { ...paletteConfig.params, stops }, { min, max });
},
[paletteService]
);
const table = data;
const accessors = getAccessorsFromArgs(args, table.columns);
@ -251,18 +302,23 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
customMetricFormatParams ?? tableMetricFormatParams ?? defaultMetricFormatParams
);
const colors = palette?.params?.colors ? normalizeColors(palette.params, min, max) : undefined;
const bands: number[] = (palette?.params as CustomPaletteState)
? normalizeBands(args.palette?.params as CustomPaletteState, { min, max })
let bands: number[] = (palette?.params as CustomPaletteState)
? normalizeBands(palette?.params as CustomPaletteState, { min, max })
: [min, max];
// TODO: format in charts
const formattedActual = Math.round(Math.min(Math.max(metricValue, min), max) * 1000) / 1000;
const goalConfig = getGoalConfig(gaugeType);
const totalTicks = getTicks(ticksPosition, [min, max], bands);
let actualValue = Math.round(Math.min(Math.max(metricValue, min), max) * 1000) / 1000;
const totalTicks = getTicks(ticksPosition, [min, max], bands, args.percentageMode);
const ticks =
gaugeType === GaugeShapes.CIRCLE ? totalTicks.slice(0, totalTicks.length - 1) : totalTicks;
if (args.percentageMode && palette?.params && palette?.params.stops?.length) {
bands = normalizeBandsLegacy(palette?.params as CustomPaletteState, actualValue);
actualValue = actualValueToPercentsLegacy(palette?.params as CustomPaletteState, actualValue);
}
const goalConfig = getGoalConfig(gaugeType);
const labelMajorTitle = getTitle(labelMajorMode, labelMajor, metricColumn?.name);
// added extra space for nice rendering
@ -271,7 +327,7 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
const extraTitles = isRoundShape(gaugeType)
? {
centralMinor: tickFormatter.convert(metricValue),
centralMinor: tickFormatter.convert(actualValue),
centralMajor: getTitle(centralMajorMode, centralMajor, metricColumn?.name),
}
: {};
@ -289,21 +345,27 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
subtype={getSubtypeByGaugeType(gaugeType)}
base={bands[0]}
target={goal && goal >= bands[0] && goal <= bands[bands.length - 1] ? goal : undefined}
actual={formattedActual}
actual={actualValue}
tickValueFormatter={({ value: tickValue }) => tickFormatter.convert(tickValue)}
tooltipValueFormatter={(tooltipValue) => tickFormatter.convert(tooltipValue)}
bands={bands}
ticks={ticks}
bandFillColor={
colorMode === GaugeColorModes.PALETTE && colors
colorMode === GaugeColorModes.PALETTE
? (val) => {
const index = bands && bands.indexOf(val.value) - 1;
return colors && index >= 0 && colors[index]
? colors[index]
: val.value <= bands[0]
? colors[0]
: colors[colors.length - 1];
// bands value is equal to the stop. The purpose of this value is coloring the previous section, which is smaller, then the band.
// So, the smaller value should be taken. For the first element -1, for the next - middle value of the previous section.
let value = val.value - 1;
const valueIndex = bands.indexOf(val.value);
if (valueIndex > 0) {
value = val.value - (bands[valueIndex] - bands[valueIndex - 1]) / 2;
}
return args.palette
? getColor(value, args.palette, bands, args.percentageMode) ?? TRANSPARENT
: TRANSPARENT;
}
: () => `rgba(255,255,255,0)`
: () => TRANSPARENT
}
labelMajor={labelMajorTitle ? `${labelMajorTitle}${majorExtraSpaces}` : labelMajorTitle}
labelMinor={labelMinor ? `${labelMinor}${minorExtraSpaces}` : ''}

View file

@ -12,7 +12,7 @@ import { ThemeServiceStart } from '../../../../../core/public';
import { KibanaThemeProvider } from '../../../../kibana_react/public';
import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers';
import { EXPRESSION_GAUGE_NAME, GaugeExpressionProps } from '../../common';
import { getFormatService, getThemeService } from '../services';
import { getFormatService, getPaletteService, getThemeService } from '../services';
interface ExpressionGaugeRendererDependencies {
theme: ThemeServiceStart;
@ -39,6 +39,7 @@ export const gaugeRenderer: (
{...config}
formatFactory={getFormatService().deserialize}
chartsThemeService={getThemeService()}
paletteService={getPaletteService()}
/>
</div>
</KibanaThemeProvider>,

View file

@ -9,7 +9,7 @@ import { ChartsPluginSetup } from '../../../charts/public';
import { CoreSetup, CoreStart } from '../../../../core/public';
import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public';
import { gaugeFunction } from '../common';
import { setFormatService, setThemeService } from './services';
import { setFormatService, setThemeService, setPaletteService } from './services';
import { gaugeRenderer } from './expression_renderers';
import type { FieldFormatsStart } from '../../../field_formats/public';
@ -28,6 +28,10 @@ export interface ExpressionGaugePluginStart {
export class ExpressionGaugePlugin {
public setup(core: CoreSetup, { expressions, charts }: ExpressionGaugePluginSetup) {
setThemeService(charts.theme);
charts.palettes.getPalettes().then((palettes) => {
setPaletteService(palettes);
});
expressions.registerFunction(gaugeFunction);
expressions.registerRenderer(gaugeRenderer({ theme: core.theme }));
}

View file

@ -7,4 +7,5 @@
*/
export { getFormatService, setFormatService } from './format_service';
export { setThemeService, getThemeService } from './theme_service';
export { getThemeService, setThemeService } from './theme_service';
export { getPaletteService, setPaletteService } from './palette_service';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createGetterSetter } from '../../../../kibana_utils/public';
import { PaletteRegistry } from '../../../../charts/public';
export const [getPaletteService, setPaletteService] =
createGetterSetter<PaletteRegistry>('palette');

View file

@ -71,6 +71,7 @@ export const toExpressionAst: VisToExpressionAst<GaugeVisParams> = (vis, params)
colorMode: 'palette',
centralMajorMode,
...(centralMajorMode === 'custom' ? { labelMinor: style.subText } : {}),
percentageMode,
});
if (colorsRange && colorsRange.length) {
@ -80,8 +81,8 @@ export const toExpressionAst: VisToExpressionAst<GaugeVisParams> = (vis, params)
range: percentageMode ? 'percent' : 'number',
continuity: 'none',
gradient: true,
rangeMax: percentageMode ? 100 : Infinity,
rangeMin: 0,
rangeMax: percentageMode ? 100 : stopsWithColors.stop[stopsWithColors.stop.length - 1],
rangeMin: stopsWithColors.stop[0],
});
gauge.addArgument('palette', buildExpression([palette]));

View file

@ -34,7 +34,7 @@ export const getStopsWithColorsFromRanges = (
) => {
return ranges.reduce<PaletteConfig>(
(acc, range, index, rangesArr) => {
if (index && range.from !== rangesArr[index - 1].to) {
if ((index && range.from !== rangesArr[index - 1].to) || index === 0) {
acc.color.push(TRANSPARENT);
acc.stop.push(range.from);
}