@gitlab/ui
Version:
GitLab UI Components
565 lines (544 loc) • 15.2 kB
JavaScript
import castArray from 'lodash/castArray';
import merge from 'lodash/merge';
import { BLUE_500 } from '../../tokens/build/js/tokens';
import { GlBreakpointInstance } from '../breakpoints';
import { columnOptions } from '../constants';
import { areDatesEqual } from '../datetime_utility';
import { engineeringNotation } from '../number_utils';
import { hexToRgba } from '../utils';
import { arrowSymbol, ANNOTATIONS_SERIES_NAME, CHART_TYPE_BAR, CHART_TYPE_LINE } from './constants';
const defaultAreaOpacity = 0.2;
const defaultFontSize = 12;
const defaultHeight = 400;
const defaultWidth = 300;
const validRenderers = ['canvas', 'svg'];
const toolboxHeight = 14;
const axes = {
name: 'Value',
type: 'value',
nameLocation: 'center'
};
const xAxis = merge({}, axes, {
boundaryGap: false,
splitLine: {
show: false
}
});
const yAxis = merge({}, axes, {
nameGap: 50,
axisLabel: {
formatter: num => engineeringNotation(num, 2)
}
});
const grid = {
top: 16,
bottom: 44,
left: 64,
right: 32
};
/**
* Options for charts where the y-axis is the metrics axis.
*/
const defaultChartOptions = {
grid,
xAxis,
yAxis,
legend: {
show: false
}
};
const gridWithSecondaryYAxis = {
...grid,
right: 64
};
const lineStyle = {
symbol: 'circle',
type: 'line',
width: 2
};
/**
* Annotations series consists of annotations lines
* along with markers. Annotations co-exist with data
* series but have their own virtual coords so that they stay put
* irrespective of data series extents.
*/
const annotationsYAxisCoords = {
min: 0,
pos: 3,
// 3% height of chart's grid
max: 100,
show: false
};
const symbolSize = 6;
/**
* These comparison operators are currently in monitoring
* charts that have alerting related data.
*
* {Array} Possible values for greater than
*/
const GREATER_THAN = ['>', '>'];
/**
* These comparison operators are currently in monitoring
* charts that have alerting related data.
*
* {Array} Possible values for less than
*/
const LESS_THAN = ['<', '<'];
/**
* All default dataZoom configs will have slider & inside
* (for reference, see https://gitlab.com/gitlab-org/gitlab-ui/issues/240)
* Inside is disabled for larger viewports (lg and xl)
* and is specifically to enable touch zoom for mobile devices
* @param {Object} options
*/
const getDataZoomConfig = function () {
let {
filterMode = 'none'
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const disabledBreakpoints = ['lg', 'xl'];
const disabled = disabledBreakpoints.includes(GlBreakpointInstance.getBreakpointSize());
const minSpan = filterMode === 'none' ? 0.01 : null;
return {
grid: {
bottom: 81
},
xAxis: {
nameGap: 67
},
dataZoom: [{
type: 'slider',
bottom: 22,
filterMode,
minSpan
}, {
type: 'inside',
filterMode,
minSpan,
disabled
}]
};
};
// All chart options can be merged but series
// needs to be concatenated.
// Series can be an object for single series or
// an array of objects.
const mergeSeriesToOptions = function (options) {
let series = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
const {
series: optSeries = []
} = options;
return {
...options,
series: [...castArray(series), ...castArray(optSeries)]
};
};
/**
* If an annotation series exists, the chart options should have an
* array of yAxis settings so that the series can exist in its own
* coordinate system without interfering with the data series
*
* @param {Object} options options to merge annotation series yAxis with
* @param {Boolean} hasAnnotations if annotation series yAxis should be merged
* @returns {Object} options
*/
const mergeAnnotationAxisToOptions = function (options) {
let hasAnnotations = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return {
...options,
...(hasAnnotations && {
yAxis: [options.yAxis, annotationsYAxisCoords]
})
};
};
const dataZoomAdjustments = dataZoom => {
// handle cases where dataZoom is array and object.
const useSlider = dataZoom && Array.isArray(dataZoom) ? dataZoom.length : Boolean(dataZoom);
return useSlider ? getDataZoomConfig({
filterMode: 'weakFilter'
}) : [];
};
/**
* Generate eCharts markArea arrays for thresholds and annotations.
*
* This method purposefully has no knowledge of comparison
* operators used in thresholds as it is not necessary and instead
* expects explict value bounds
*
* Examples:
* { min: 7, max: 10 } => markArea from 7 to 10
* { min: 1, max: 7 } => markArea from 1 to 7
*
* If min and max are equal it would be markLine and would be
* generated by `generateMarkLines`
*
* @param {Object} threshold Threshold/Annotation object with min and max values
* @param {String} axis markArea is generated against this axis
* @returns {Array}
*/
const generateMarkArea = function (_ref) {
let {
min,
max
} = _ref;
let axis = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yAxis';
return [{
[axis]: min
}, {
[axis]: max
}];
};
/**
* Generate eCharts markLine objects for thresholds and annotations.
*
* This method purposefully has no knowledge of comparison
* operators used in thresholds as it is not necessary and instead
* expects explict value bounds
*
* In order to continue supporting existing thresholds format, min
* is passed as undefined so the correct markLine object is generated.
*
* For annotations, min and max will be the same value.
*
* Threshold Examples:
* { max: 7 } => markLine at 7
*
* Annotation Examples:
* { min: 7, max: 7 } => markLine at 7
*
* @param {Object} threshold Threshold/Annotation object with min and max values
* @param {String} axis markLine is generated against this axis
* @returns {Object}
*/
const generateMarkLines = function (_ref2) {
let {
min,
max
} = _ref2;
let axis = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yAxis';
if (min) {
return {
[axis]: min
};
}
return {
[axis]: max
};
};
/**
* Generates markPoints that are placed under the markLines.
*
* These are used only in annotation lines. For annotation lines,
* both min and max are same values so only one is enough to generate
* the markPoint.
*
* @param {Object} annotation object
* @return {Object}
*/
const generateMarkPoints = _ref3 => {
let {
min,
tooltipData
} = _ref3;
return {
name: 'annotations',
xAxis: min,
yAxis: 0,
tooltipData
};
};
/**
* Generate set of markAreas and markLines to draw on charts
* as alert thresholds.
*
* Alert thresholds always have a markLine associated with a markArea
*
* @param {Array} thresholds Array of alert thresholds
* @returns {Object} markAreas and markLines
*/
function getThresholdConfig(thresholds) {
if (!thresholds.length) {
return {};
}
const data = thresholds.reduce((acc, alert) => {
const {
threshold,
operator
} = alert;
if (GREATER_THAN.includes(operator)) {
acc.areas.push(generateMarkArea({
min: threshold,
max: Infinity
}));
} else if (LESS_THAN.includes(operator)) {
acc.areas.push(generateMarkArea({
min: Number.NEGATIVE_INFINITY,
max: threshold
}));
}
acc.lines.push(generateMarkLines({
max: threshold
}));
return acc;
}, {
lines: [],
areas: []
});
return {
markLine: {
data: data.lines
},
markArea: {
data: data.areas,
zlevel: -1
}
};
}
/**
* This method is only for testing both markLines and markAreas
* that are used for annotations.
*
* `getAnnotationsConfig` as of %12.10 supports only markLines.
* But this method can generate lines, points and areas.
*
* @param {Array} annotations Array of annotation objects
* @returns {Object} { areas, lines, points }
*/
const parseAnnotations = annotations => annotations.reduce((acc, annotation) => {
// because only markLines are supported all cases will
// satisfy this condition. This is more of a sanity check
// until markAreas are supported.
// https://gitlab.com/gitlab-org/gitlab/-/issues/212910
if (areDatesEqual(annotation.min, annotation.max)) {
acc.lines.push(generateMarkLines(annotation, 'xAxis'));
acc.points.push(generateMarkPoints(annotation));
return acc;
}
acc.areas.push(generateMarkArea(annotation, 'xAxis'));
return acc;
}, {
areas: [],
lines: [],
points: []
});
/**
* Generate set of markAreas and markLines to draw on charts
* as annotations.
*
* Annotations as of %12.10 will only be markLines.
* markAreas are not supported yet. They are generated by
* `parseAnnotations` but not rendered.
*
* @param {Array} annotations Array of annotations
* @returns {Object} { markLines }
*/
const getAnnotationsConfig = annotations => {
if (!annotations.length) {
return {};
}
// annotations parsing is moved out so that it can be tested
// for markLines and markAreas.
const {
lines,
points
} = parseAnnotations(annotations);
return {
markLine: {
lineStyle: {
color: BLUE_500
},
silent: true,
data: lines
},
markPoint: {
itemStyle: {
color: BLUE_500
},
symbol: arrowSymbol,
symbolSize: '8',
symbolOffset: [0, ' 60%'],
data: points
}
};
};
/**
* Given thresholds and annotations options, this method generates
* an annotation series that co-exists along with the data series.
*
* yAxis option is useful in cases where multiple yAxis settings
* are used in a chart. Currently, all of our charts have single
* yAxis settings.
*
* @param {Object} params Thresholds, annotations and yAxis options
* @returns {Object} Annotation series
*/
const generateAnnotationSeries = function (annotations) {
let yAxisIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
if (!annotations.length) {
return null;
}
return merge({
name: ANNOTATIONS_SERIES_NAME,
yAxisIndex,
type: 'scatter',
data: []
}, getAnnotationsConfig(annotations));
};
/**
* This method generates the data series and relevant defaults for a bar chart
*
* @param {Object} options
* @param {string} options.name - xAxis name for the chart
* @param {string} options.color - color to render the data series
* @param {Array} options.data - data set to be rendered
* @param {string} [options.stack] - controls how the stacked charts should render either `stacked` or `tiled`
* @param {number} [options.yAxisIndex] - specifies the yAxis to use (if there are multiple)
* @returns {Object} Bar chart series
*/
const generateBarSeries = _ref4 => {
let {
name,
color,
data = [],
stack = columnOptions.stacked,
yAxisIndex = 0
} = _ref4;
return {
type: CHART_TYPE_BAR,
name,
data,
stack,
barMaxWidth: '50%',
yAxisIndex,
itemStyle: {
color: hexToRgba(color, 0.2),
borderColor: color,
borderWidth: 1
},
emphasis: {
itemStyle: {
color: hexToRgba(color, 0.4)
}
}
};
};
/**
* This method generates the data series and relevant defaults for a line chart
*
* @param {Object} options
* @param {string} options.name - xAxis name for the chart
* @param {string} options.color - color to render the data series
* @param {Array} options.data - data set to be rendered
* @param {number} [options.yAxisIndex] - specifies the yAxis to use (if there are multiple)
* @returns {Object} Line chart series
*/
const generateLineSeries = _ref5 => {
let {
name,
color,
data = [],
yAxisIndex = 0
} = _ref5;
return {
name,
data,
type: CHART_TYPE_LINE,
yAxisIndex,
lineStyle: {
color
},
itemStyle: {
color
}
};
};
const getTooltipTitle = function () {
let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
let titleAxisName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (!params) return '';
const title = params.seriesData.reduce((acc, _ref6) => {
let {
value
} = _ref6;
if (acc.includes(value[0])) {
return acc;
}
return [...acc, value[0]];
}, []).join(', ');
return titleAxisName ? `${title} (${titleAxisName})` : title;
};
const getTooltipContent = function () {
let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
let valueAxisName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (!params) {
return {};
}
const {
seriesData
} = params;
if (seriesData.length === 1) {
const {
value: [, yValue],
seriesName
} = seriesData[0];
return {
[valueAxisName || seriesName]: {
value: yValue,
color: '' // ignore color when showing a single series
}
};
}
return seriesData.reduce((acc, _ref7) => {
let {
value,
seriesName,
color
} = _ref7;
const yValue = value[1];
acc[seriesName] = {
value: yValue,
color
};
return acc;
}, {});
};
/**
* The method works well if tooltip content should be against y-axis values.
* However, for bar charts, the tooltip should be against x-axis values.
* This method should be updated to work with all types of visualizations.
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/674
*
* @param {Object} params series data
* @param {String} yAxisTitle y-axis title
* @returns {Object} tooltip title and content
* @deprecated Use getTooltipContent and getTooltipContent to obtain the tooltip
*/
const getDefaultTooltipContent = function (params) {
let yAxisTitle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
const seriesDataLength = params.seriesData.length;
const {
xLabels,
tooltipContent
} = params.seriesData.reduce((acc, chartItem) => {
const [title, value] = chartItem.value || [];
// Let's use the y axis title as series name when only one series exists
// This way, TooltipDefaultFormat will display the y axis title as label
const seriesName = seriesDataLength === 1 && yAxisTitle ? yAxisTitle : chartItem.seriesName;
const color = seriesDataLength === 1 ? '' : chartItem.color;
acc.tooltipContent[seriesName] = {
value,
color
};
if (!acc.xLabels.includes(title)) {
acc.xLabels.push(title);
}
return acc;
}, {
xLabels: [],
tooltipContent: {}
});
return {
xLabels,
tooltipContent
};
};
export { annotationsYAxisCoords, axes, dataZoomAdjustments, defaultAreaOpacity, defaultChartOptions, defaultFontSize, defaultHeight, defaultWidth, generateAnnotationSeries, generateBarSeries, generateLineSeries, getAnnotationsConfig, getDataZoomConfig, getDefaultTooltipContent, getThresholdConfig, getTooltipContent, getTooltipTitle, grid, gridWithSecondaryYAxis, lineStyle, mergeAnnotationAxisToOptions, mergeSeriesToOptions, parseAnnotations, symbolSize, toolboxHeight, validRenderers, xAxis, yAxis };