UNPKG

@gitlab/ui

Version:
565 lines (544 loc) • 15.2 kB
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 = ['>', '&gt;']; /** * These comparison operators are currently in monitoring * charts that have alerting related data. * * {Array} Possible values for less than */ const LESS_THAN = ['<', '&lt;']; /** * 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 };