Commit 364e813c authored by Miguel Rincon's avatar Miguel Rincon

Adds `y_axis.format` and `y_axis.precision` to dashboard yml

- y_axis is now supported to contain a `format` and `name`.
- chart grid size adjusted to fit labels more comfortably
- tooltip format takes format of y axis

Internally, axis options are computed in a separate file for
easier sharing between components.
parent e205d28f
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { s__ } from '~/locale';
const yAxisBoundaryGap = [0.1, 0.1];
/**
* Max string length of formatted axis tick
*/
const maxDataAxisTickLength = 8;
// Defaults
const defaultFormat = SUPPORTED_FORMATS.number;
const defaultYAxisFormat = defaultFormat;
const defaultYAxisPrecision = 2;
const defaultTooltipFormat = defaultFormat;
const defaultTooltipPrecision = 3;
// Give enough space for y-axis with units and name.
const chartGridLeft = 75;
// Axis options
/**
* Converts .yml parameters to echarts axis options for data axis
* @param {Object} param - Dashboard .yml definition options
*/
const getDataAxisOptions = ({ format, precision, name }) => {
const formatter = getFormatter(format);
return {
name,
nameLocation: 'center', // same as gitlab-ui's default
scale: true,
axisLabel: {
formatter: val => formatter(val, precision, maxDataAxisTickLength),
},
};
};
/**
* Converts .yml parameters to echarts y-axis options
* @param {Object} param - Dashboard .yml definition options
*/
export const getYAxisOptions = ({
name = s__('Metrics|Values'),
format = defaultYAxisFormat,
precision = defaultYAxisPrecision,
} = {}) => {
return {
nameGap: 63, // larger gap than gitlab-ui's default to fit with formatted numbers
scale: true,
boundaryGap: yAxisBoundaryGap,
...getDataAxisOptions({
name,
format,
precision,
}),
};
};
// Chart grid
/**
* Grid with enough room to display chart.
*/
export const getChartGrid = ({ left = chartGridLeft } = {}) => ({ left });
// Tooltip options
export const getTooltipFormatter = ({
format = defaultTooltipFormat,
precision = defaultTooltipPrecision,
} = {}) => {
const formatter = getFormatter(format);
return num => formatter(num, precision);
};
...@@ -4,7 +4,6 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ ...@@ -4,7 +4,6 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getFormatter } from '~/lib/utils/unit_format';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { import {
...@@ -16,6 +15,7 @@ import { ...@@ -16,6 +15,7 @@ import {
dateFormats, dateFormats,
chartColorValues, chartColorValues,
} from '../../constants'; } from '../../constants';
import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { makeDataSeries } from '~/helpers/monitor_helper'; import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils'; import { graphDataValidatorForValues } from '../../utils';
...@@ -30,15 +30,13 @@ const deploymentYAxisCoords = { ...@@ -30,15 +30,13 @@ const deploymentYAxisCoords = {
max: 100, max: 100,
}; };
const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds const THROTTLED_DATAZOOM_WAIT = 1000; // milliseconds
const timestampToISODate = timestamp => new Date(timestamp).toISOString(); const timestampToISODate = timestamp => new Date(timestamp).toISOString();
const events = { const events = {
datazoom: 'datazoom', datazoom: 'datazoom',
}; };
const yValFormatter = getFormatter('number');
export default { export default {
components: { components: {
GlAreaChart, GlAreaChart,
...@@ -167,14 +165,7 @@ export default { ...@@ -167,14 +165,7 @@ export default {
const option = omit(this.option, ['series', 'yAxis', 'xAxis']); const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
const dataYAxis = { const dataYAxis = {
name: this.yAxisLabel, ...getYAxisOptions(this.graphData.yAxis),
nameGap: 50, // same as gitlab-ui's default
nameLocation: 'center', // same as gitlab-ui's default
boundaryGap: [0.1, 0.1],
scale: true,
axisLabel: {
formatter: num => yValFormatter(num, 3),
},
...yAxis, ...yAxis,
}; };
...@@ -204,6 +195,7 @@ export default { ...@@ -204,6 +195,7 @@ export default {
series: this.chartOptionSeries, series: this.chartOptionSeries,
xAxis: timeXAxis, xAxis: timeXAxis,
yAxis: [dataYAxis, deploymentsYAxis], yAxis: [dataYAxis, deploymentsYAxis],
grid: getChartGrid(),
dataZoom: [this.dataZoomConfig], dataZoom: [this.dataZoomConfig],
...option, ...option,
}; };
...@@ -282,8 +274,9 @@ export default { ...@@ -282,8 +274,9 @@ export default {
}, },
}; };
}, },
yAxisLabel() { tooltipYFormatter() {
return `${this.graphData.y_label}`; // Use same format as y-axis
return getTooltipFormatter({ format: this.graphData.yAxis?.format });
}, },
}, },
created() { created() {
...@@ -315,12 +308,11 @@ export default { ...@@ -315,12 +308,11 @@ export default {
this.tooltip.commitUrl = deploy.commitUrl; this.tooltip.commitUrl = deploy.commitUrl;
} else { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
const value = yValFormatter(yVal, 3);
this.tooltip.content.push({ this.tooltip.content.push({
name: seriesName, name: seriesName,
dataIndex, dataIndex,
value, value: this.tooltipYFormatter(yVal),
color, color,
}); });
} }
......
import { slugify } from '~/lib/utils/text_utility'; import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export const gqClient = createGqClient( export const gqClient = createGqClient(
...@@ -74,18 +75,38 @@ const mapToMetricsViewModel = (metrics, defaultLabel) => ...@@ -74,18 +75,38 @@ const mapToMetricsViewModel = (metrics, defaultLabel) =>
...metric, ...metric,
})); }));
/**
* Maps an axis view model
*
* Defaults to a 2 digit precision and `number` format. It only allows
* formats in the SUPPORTED_FORMATS array.
*
* @param {Object} axis
*/
const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => {
return {
name,
format: SUPPORTED_FORMATS[format] || SUPPORTED_FORMATS.number,
precision,
};
};
/** /**
* Maps a metrics panel to its view model * Maps a metrics panel to its view model
* *
* @param {Object} panel - Metrics panel * @param {Object} panel - Metrics panel
* @returns {Object} * @returns {Object}
*/ */
const mapToPanelViewModel = ({ title = '', type, y_label, metrics = [] }) => { const mapToPanelViewModel = ({ title = '', type, y_label, y_axis = {}, metrics = [] }) => {
// Both `y_axis.name` and `y_label` are supported for now
// https://gitlab.com/gitlab-org/gitlab/issues/208385
const yAxis = mapToAxisViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
return { return {
title, title,
type, type,
y_label, y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198
metrics: mapToMetricsViewModel(metrics, y_label), yAxis,
metrics: mapToMetricsViewModel(metrics, yAxis.name),
}; };
}; };
......
---
title: Add properties to the dashboard definition to customize y-axis format
merge_request: 25785
author:
type: added
...@@ -12432,6 +12432,9 @@ msgstr "" ...@@ -12432,6 +12432,9 @@ msgstr ""
msgid "Metrics|Validating query" msgid "Metrics|Validating query"
msgstr "" msgstr ""
msgid "Metrics|Values"
msgstr ""
msgid "Metrics|View logs" msgid "Metrics|View logs"
msgstr "" msgstr ""
......
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options';
describe('options spec', () => {
describe('getYAxisOptions', () => {
it('default options', () => {
const options = getYAxisOptions();
expect(options).toMatchObject({
name: expect.any(String),
axisLabel: {
formatter: expect.any(Function),
},
scale: true,
boundaryGap: [expect.any(Number), expect.any(Number)],
});
expect(options.name).not.toHaveLength(0);
});
it('name options', () => {
const yAxisName = 'My axis values';
const options = getYAxisOptions({
name: yAxisName,
});
expect(options).toMatchObject({
name: yAxisName,
nameLocation: 'center',
nameGap: expect.any(Number),
});
});
it('formatter options', () => {
const options = getYAxisOptions({
format: SUPPORTED_FORMATS.bytes,
});
expect(options.axisLabel.formatter).toEqual(expect.any(Function));
expect(options.axisLabel.formatter(1)).toBe('1.00B');
});
});
describe('getTooltipFormatter', () => {
it('default format', () => {
const formatter = getTooltipFormatter();
expect(formatter).toEqual(expect.any(Function));
expect(formatter(1)).toBe('1.000');
});
it('defined format', () => {
const formatter = getTooltipFormatter({
format: SUPPORTED_FORMATS.bytes,
});
expect(formatter(1)).toBe('1.000B');
});
});
});
...@@ -190,7 +190,8 @@ describe('Time series component', () => { ...@@ -190,7 +190,8 @@ describe('Time series component', () => {
it('formats tooltip content', () => { it('formats tooltip content', () => {
const name = 'Total'; const name = 'Total';
const value = '5.556'; const value = '5.556MB';
const dataIndex = 0; const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
...@@ -348,9 +349,9 @@ describe('Time series component', () => { ...@@ -348,9 +349,9 @@ describe('Time series component', () => {
}); });
}); });
it('additional y axis data', () => { it('additional y-axis data', () => {
const mockCustomYAxisOption = { const mockCustomYAxisOption = {
name: 'Custom y axis label', name: 'Custom y-axis label',
axisLabel: { axisLabel: {
formatter: jest.fn(), formatter: jest.fn(),
}, },
...@@ -397,8 +398,8 @@ describe('Time series component', () => { ...@@ -397,8 +398,8 @@ describe('Time series component', () => {
deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
}); });
it('rounds to 3 decimal places', () => { it('formats and rounds to 2 decimal places', () => {
expect(dataFormatter(0.88888)).toBe('0.889'); expect(dataFormatter(0.88888)).toBe('0.89MB');
}); });
it('deployment formatter is set as is required to display a tooltip', () => { it('deployment formatter is set as is required to display a tooltip', () => {
...@@ -421,7 +422,7 @@ describe('Time series component', () => { ...@@ -421,7 +422,7 @@ describe('Time series component', () => {
}); });
describe('yAxisLabel', () => { describe('yAxisLabel', () => {
it('y axis is configured correctly', () => { it('y-axis is configured correctly', () => {
const { yAxis } = getChartOptions(); const { yAxis } = getChartOptions();
expect(yAxis).toHaveLength(2); expect(yAxis).toHaveLength(2);
......
...@@ -393,13 +393,16 @@ export const metricsDashboardPayload = { ...@@ -393,13 +393,16 @@ export const metricsDashboardPayload = {
type: 'area-chart', type: 'area-chart',
y_label: 'Total Memory Used', y_label: 'Total Memory Used',
weight: 4, weight: 4,
y_axis: {
format: 'megabytes',
},
metrics: [ metrics: [
{ {
id: 'system_metrics_kubernetes_container_memory_total', id: 'system_metrics_kubernetes_container_memory_total',
query_range: query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1000/1000',
label: 'Total', label: 'Total',
unit: 'GB', unit: 'MB',
metric_id: 12, metric_id: 12,
prometheus_endpoint_path: 'http://test', prometheus_endpoint_path: 'http://test',
}, },
......
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { import {
uniqMetricsId, uniqMetricsId,
parseEnvironmentsResponse, parseEnvironmentsResponse,
...@@ -44,6 +45,11 @@ describe('mapToDashboardViewModel', () => { ...@@ -44,6 +45,11 @@ describe('mapToDashboardViewModel', () => {
title: 'Title A', title: 'Title A',
type: 'chart-type', type: 'chart-type',
y_label: 'Y Label A', y_label: 'Y Label A',
yAxis: {
name: 'Y Label A',
format: 'number',
precision: 2,
},
metrics: [], metrics: [],
}, },
], ],
...@@ -90,6 +96,98 @@ describe('mapToDashboardViewModel', () => { ...@@ -90,6 +96,98 @@ describe('mapToDashboardViewModel', () => {
}); });
}); });
describe('panel mapping', () => {
const panelTitle = 'Panel Title';
const yAxisName = 'Y Axis Name';
let dashboard;
const setupWithPanel = panel => {
dashboard = {
panel_groups: [
{
panels: [panel],
},
],
};
};
const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0];
it('group y_axis defaults', () => {
setupWithPanel({
title: panelTitle,
});
expect(getMappedPanel()).toEqual({
title: panelTitle,
y_label: '',
yAxis: {
name: '',
format: SUPPORTED_FORMATS.number,
precision: 2,
},
metrics: [],
});
});
it('panel with y_axis.name', () => {
setupWithPanel({
y_axis: {
name: yAxisName,
},
});
expect(getMappedPanel().y_label).toBe(yAxisName);
expect(getMappedPanel().yAxis.name).toBe(yAxisName);
});
it('panel with y_axis.name and y_label, displays y_axis.name', () => {
setupWithPanel({
y_label: 'Ignored Y Label',
y_axis: {
name: yAxisName,
},
});
expect(getMappedPanel().y_label).toBe(yAxisName);
expect(getMappedPanel().yAxis.name).toBe(yAxisName);
});
it('group y_label', () => {
setupWithPanel({
y_label: yAxisName,
});
expect(getMappedPanel().y_label).toBe(yAxisName);
expect(getMappedPanel().yAxis.name).toBe(yAxisName);
});
it('group y_axis format and precision', () => {
setupWithPanel({
title: panelTitle,
y_axis: {
precision: 0,
format: SUPPORTED_FORMATS.bytes,
},
});
expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.bytes);
expect(getMappedPanel().yAxis.precision).toBe(0);
});
it('group y_axis unsupported format defaults to number', () => {
setupWithPanel({
title: panelTitle,
y_axis: {
format: 'invalid_format',
},
});
expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.number);
});
});
describe('metrics mapping', () => { describe('metrics mapping', () => {
const defaultLabel = 'Panel Label'; const defaultLabel = 'Panel Label';
const dashboardWithMetric = (metric, label = defaultLabel) => ({ const dashboardWithMetric = (metric, label = defaultLabel) => ({
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment