/* * << * Davinci * == * Copyright (C) 2016 - 2017 EDP * == * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * >> */ import mean from 'lodash/mean' import { IAxisConfig } from '../../components/Workbench/ConfigSections/AxisSection' import { ILabelConfig } from '../../components/Workbench/ConfigSections/LabelSection' import { ILegendConfig } from '../../components/Workbench/ConfigSections/LegendSection' import { getFormattedValue } from '../../components/Config/Format' import { CHART_LEGEND_POSITIONS, DEFAULT_SPLITER } from 'app/globalConstants' import { EChartOption } from 'echarts' import { IWidgetMetric } from '../../components/Widget' import { metricAxisLabelFormatter, decodeMetricName, getTextWidth, getAggregatorLocale } from '../../components/util' import { FieldSortTypes } from '../../components/Config/Sort' import { getFieldAlias } from '../../components/Config/Field' import { IReference, IReferenceLineData, IReferenceBandData } from '../../components/Workbench/Reference/types' import { ReferenceType, ReferenceValueType } from '../../components/Workbench/Reference/constants' import ChartTypes from '../../config/chart/ChartTypes' interface ISplitLineConfig { showLine: boolean lineStyle: 'solid' | 'dashed' | 'dotted' lineSize: string lineColor: string } export function getDimetionAxisOption( dimetionAxisConfig: IAxisConfig, splitLineConfig: ISplitLineConfig, data: string[] ): EChartOption.XAxis { const { inverse, showLine: showLineX, lineStyle: lineStyleX, lineSize: lineSizeX, lineColor: lineColorX, showLabel: showLabelX, labelFontFamily: labelFontFamilyX, labelFontSize: labelFontSizeX, labelColor: labelColorX, nameLocation, nameGap, nameRotate, showInterval, xAxisInterval, xAxisRotate } = dimetionAxisConfig const { showLine, lineStyle, lineSize, lineColor } = splitLineConfig const intervalOption = showInterval ? { interval: xAxisInterval } : null return { data, inverse, axisLabel: { show: showLabelX, color: labelColorX, fontFamily: labelFontFamilyX, fontSize: Number(labelFontSizeX), rotate: xAxisRotate, ...intervalOption }, axisLine: { show: showLineX, lineStyle: { color: lineColorX, width: Number(lineSizeX), type: lineStyleX } }, axisTick: { show: showLabelX, lineStyle: { color: lineColorX } }, splitLine: { show: showLine, lineStyle: { color: lineColor, width: Number(lineSize), type: lineStyle } }, nameLocation, nameRotate, nameGap } } export function getMetricAxisOption( metricAxisConfig: IAxisConfig, splitLineConfig: ISplitLineConfig, title: string, axis: 'x' | 'y' = 'y', percentage?: boolean ): EChartOption.YAxis { const { inverse, showLine: showLineY, lineStyle: lineStyleY, lineSize: lineSizeY, lineColor: lineColorY, showLabel: showLabelY, labelFontFamily: labelFontFamilyY, labelFontSize: labelFontSizeY, labelColor: labelColorY, showTitleAndUnit, titleFontFamily, titleFontSize, titleColor, nameLocation, nameRotate, nameGap, min, max } = metricAxisConfig const { showLine, lineStyle, lineSize, lineColor } = splitLineConfig return { type: 'value', inverse, min: percentage ? 0 : min, max: percentage ? 100 : max, axisLabel: { show: showLabelY, color: labelColorY, fontFamily: labelFontFamilyY, fontSize: Number(labelFontSizeY), formatter: percentage ? '{value}%' : metricAxisLabelFormatter }, axisLine: { show: showLineY, lineStyle: { color: lineColorY, width: Number(lineSizeY), type: lineStyleY } }, axisTick: { show: showLabelY, lineStyle: { color: lineColorY } }, name: showTitleAndUnit ? title : '', nameLocation, nameGap, nameRotate, nameTextStyle: { color: titleColor, fontFamily: titleFontFamily, fontSize: Number(titleFontSize) }, splitLine: { show: showLine, lineStyle: { color: lineColor, width: Number(lineSize), type: lineStyle } } } } export function getLabelOption( type: string, labelConfig: ILabelConfig, metrics, emphasis?: boolean, options?: object ) { const { showLabel, labelPosition, labelFontFamily, labelFontSize, labelColor, pieLabelPosition, funnelLabelPosition } = labelConfig let position switch (type) { case 'pie': position = pieLabelPosition break case 'funnel': position = funnelLabelPosition break default: position = labelPosition break } let formatter switch (type) { case 'line': formatter = (params) => { const { value, seriesId } = params const m = metrics.find( (m) => m.name === seriesId.split(`${DEFAULT_SPLITER}${DEFAULT_SPLITER}`)[0] ) const formattedValue = getFormattedValue(value, m.format) return formattedValue } break case 'waterfall': formatter = (params) => { const { value } = params const formattedValue = getFormattedValue(value, metrics[0].format) return formattedValue } break case 'scatter': formatter = (params) => { const { value } = params const formattedValue = getFormattedValue(value[0], metrics[0].format) return formattedValue } break case 'pie': case 'funnel': formatter = (params) => { const { name, value, percent, dataIndex, data } = params const formattedValue = getFormattedValue( value, metrics[metrics.length > 1 ? dataIndex : 0].format ) const { labelParts } = labelConfig if (!labelParts) { return `${name}\n${formattedValue}(${percent}%)` } const labels: string[] = [] const multiRate = labelParts.filter((label) => ['percentage', 'conversion', 'arrival'].includes(label) ).length > 1 if (labelParts.includes('dimensionValue')) { labels.push(name) } if (labelParts.includes('indicatorValue')) { labels.push(formattedValue) } if (labelParts.includes('conversion') && data.conversion) { labels.push(`${multiRate ? '转化率:' : ''}${data.conversion}%`) } if (labelParts.includes('arrival') && data.arrival) { labels.push(`${multiRate ? '到达率:' : ''}${data.arrival}%`) } if (labelParts.includes('percentage')) { labels.push(`${multiRate ? '百分比:' : ''}${percent}%`) } return labels.join('\n') } break case 'radar': formatter = (params) => { const { name, value, dataIndex, data } = params const metricIdx = data.name ? dataIndex : data.value.indexOf(value) const formattedValue = getFormattedValue( value, metrics[metricIdx].format ) const labelName = name || getFieldAlias(metrics[metricIdx].field, {}) || decodeMetricName(metrics[metricIdx].name) const { labelParts } = labelConfig if (!labelParts) { return `${labelName}\n${formattedValue}` } const labels: string[] = [] if (labelParts.includes('indicatorName')) { labels.push(labelName) } if (labelParts.includes('indicatorValue')) { labels.push(formattedValue) } if (labels.length > 1) { labels.splice(1, 0, '\n') } return labels.join('') } break case 'lines': formatter = (param) => { const { name, data } = param return `${name}(${data.value[2]})` } break } const labelOption = { normal: { show: type === 'pie' && pieLabelPosition === 'center' ? false : showLabel, position, distance: 15, color: labelColor, fontFamily: labelFontFamily, fontSize: labelFontSize, formatter, ...options }, ...(emphasis && { emphasis: { show: showLabel, position, distance: 15, color: labelColor, fontFamily: labelFontFamily, fontSize: labelFontSize, formatter, ...options } }) } return labelOption } export function getLegendOption( legendConfig: ILegendConfig, seriesNames: string[] ) { const { showLegend, legendPosition, legendType, selectAll, fontFamily, fontSize, color } = legendConfig let orient let positions let type switch (legendType) { case 'plain': type = 'plain' break case 'scroll': type = 'scroll' break default: break } switch (legendPosition) { case 'top': orient = { orient: 'horizontal' } positions = { top: 8, left: 8, right: 8, height: 32 } break case 'bottom': orient = { orient: 'horizontal' } positions = { bottom: 8, left: 8, right: 8, height: 32 } break case 'left': orient = { orient: 'vertical' } positions = { left: 8, top: 16, bottom: 24, width: 96 } break default: orient = { orient: 'vertical' } positions = { right: 8, top: 16, bottom: 24, width: 96 } break } const selected = { selected: seriesNames.reduce( (obj, name) => ({ ...obj, [name]: selectAll }), {} ) } return { show: showLegend && seriesNames.length > 1, data: seriesNames, type, textStyle: { fontFamily, fontSize, color }, ...orient, ...positions, ...selected } } export function getGridPositions( legendConfig: Partial, seriesNames, chartName?: string, isHorizontalBar?: boolean, yAxisConfig?: IAxisConfig, dimetionAxisConfig?: IAxisConfig, xAxisData?: string[] ) { const { showLegend, legendPosition, fontSize } = legendConfig return CHART_LEGEND_POSITIONS.reduce((grid, pos) => { const val = pos.value grid[val] = getGridBase( val, chartName, dimetionAxisConfig, xAxisData, isHorizontalBar, yAxisConfig ) if (showLegend && seriesNames.length > 1) { grid[val] += legendPosition === val ? ['top', 'bottom'].includes(val) ? 64 : 64 + Math.max( ...seriesNames.map((s) => getTextWidth(s, '', `${fontSize}px`)) ) : 0 } return grid }, {}) } function getGridBase( pos, chartName, dimetionAxisConfig?: IAxisConfig, xAxisData?: string[], isHorizontalBar?: boolean, yAxisConfig?: IAxisConfig ) { const labelFontSize = dimetionAxisConfig ? dimetionAxisConfig.labelFontSize : 12 const xAxisRotate = dimetionAxisConfig ? dimetionAxisConfig.xAxisRotate : 0 const maxWidth = xAxisData && xAxisData.length ? Math.max( ...xAxisData.map((s) => getTextWidth(s, '', `${labelFontSize}px`)) ) : 0 const bottomDistance = dimetionAxisConfig && dimetionAxisConfig.showLabel ? isHorizontalBar ? 50 : xAxisRotate ? 50 + Math.sin((xAxisRotate * Math.PI) / 180) * maxWidth : 50 : 50 const yAxisConfigLeft = yAxisConfig && !yAxisConfig.showLabel && !yAxisConfig.showTitleAndUnit ? 24 : 64 const leftDistance = dimetionAxisConfig && dimetionAxisConfig.showLabel ? isHorizontalBar ? xAxisRotate === void 0 ? 64 : 24 + Math.cos((xAxisRotate * Math.PI) / 180) * maxWidth : yAxisConfigLeft : isHorizontalBar ? 24 : yAxisConfigLeft switch (pos) { case 'top': return 24 case 'left': return leftDistance case 'right': return chartName === 'doubleYAxis' ? 64 : 24 case 'bottom': return bottomDistance } } export function makeGrouped( data: object[], groupColumns: string[], xAxisColumn: string, metrics: IWidgetMetric[], xAxisData: string[] ) { const grouped = {} data.forEach((d) => { const groupingKey = groupColumns.map((col) => d[col]).join(' ') const colKey = d[xAxisColumn] || 'default' if (!grouped[groupingKey]) { grouped[groupingKey] = {} } if (!grouped[groupingKey][colKey]) { grouped[groupingKey][colKey] = [] } grouped[groupingKey][colKey].push(d) }) Object.keys(grouped).forEach((groupingKey) => { const currentGroupValues = grouped[groupingKey] grouped[groupingKey] = xAxisData.length ? xAxisData.map((xd) => { if (currentGroupValues[xd]) { return currentGroupValues[xd][0] } else { return metrics.reduce( (obj, m) => ({ ...obj, [`${m.agg}(${decodeMetricName(m.name)})`]: 0 }), { [xAxisColumn]: xd // []: groupingKey } ) } }) : [currentGroupValues['default'][0]] }) return grouped } // TODO: function explanation export function getGroupedXaxis(data, xAxisColumn, metrics) { if (xAxisColumn) { const metricsInSorting = metrics.filter( ({ sort }) => sort && sort.sortType !== FieldSortTypes.Default ) const appliedMetric = metricsInSorting.length ? metricsInSorting[0] : void 0 const dataGroupByXaxis = data.reduce((grouped, d) => { const colKey = d[xAxisColumn] if (grouped[colKey] === void 0) { grouped[colKey] = 0 } if (appliedMetric) { const { agg, name } = appliedMetric grouped[colKey] += d[`${agg}(${decodeMetricName(name)})`] } return grouped }, {}) if (appliedMetric) { return Object.entries(dataGroupByXaxis) .sort((p1: [string, number], p2: [string, number]) => { return appliedMetric.sort.sortType === FieldSortTypes.Asc ? p1[1] - p2[1] : appliedMetric.sort.sortType === FieldSortTypes.Desc ? p2[1] - p1[1] : 0 }) .map(([key, value]) => key) } else { return Object.keys(dataGroupByXaxis) } } return [] } export function getSymbolSize(sizeRate, size) { return sizeRate ? Math.ceil(size / sizeRate) : size } export function getCartesianChartMetrics(metrics: IWidgetMetric[]) { return metrics.map((metric) => { const { name, agg } = metric const decodedMetricName = decodeMetricName(name) const duplicates = metrics.filter( (m) => decodeMetricName(m.name) === decodedMetricName && m.agg === agg ) const prefix = agg !== 'sum' ? `[${getAggregatorLocale(agg)}] ` : '' const suffix = duplicates.length > 1 ? duplicates.indexOf(metric) ? duplicates.indexOf(metric) + 1 : '' : '' return { ...metric, displayName: `${prefix}${decodedMetricName}${suffix}` } }) } export function getCartesianChartReferenceOptions( references: IReference[], chartType: ChartTypes, metrics: IWidgetMetric[], sourcedata: any[], barChart?: boolean ) { if (references) { const markLines = [] const markAreas = [] references.forEach((ref) => { const { name, type, data } = ref if (type === ReferenceType.Line) { const { metric, type: valueType, value, label, line } = data as IReferenceLineData const axis = getReferenceDataMetricAxis(chartType, { barChart, metrics, metric }) if (axis) { const metricData = sourcedata.map((d) => { const metricObject = metrics.find((m) => m.name === metric) return ( metricObject && d[`${metricObject.agg}(${decodeMetricName(metric)})`] ) }) markLines.push({ ...getReferenceDataOptions(metricData, valueType, value, axis), name, label: { show: label.visible, position: label.position, color: label.font.color, fontSize: label.font.size, fontFamily: label.font.family }, lineStyle: { color: line.color, width: line.width, type: line.type } }) } } else { const areaData = (data as [IReferenceBandData, IReferenceBandData]).map( (d, index) => { const { metric, type: valueType, value, label, band } = d const axis = getReferenceDataMetricAxis(chartType, { barChart, metrics, metric }) if (axis) { const metricData = sourcedata.map((d) => { const metricObject = metrics.find((m) => m.name === metric) return ( metricObject && d[`${metricObject.agg}(${decodeMetricName(metric)})`] ) }) const dataOptions = getReferenceDataOptions( metricData, valueType, value, axis ) return !index ? dataOptions : { ...dataOptions, name, label: { show: label.visible, position: label.position, color: label.font.color, fontSize: label.font.size, fontFamily: label.font.family }, emphasis: { label: { position: label.position } }, itemStyle: { color: band.color, borderColor: band.border.color, borderWidth: band.border.width, borderType: band.border.type } } } else { return void 0 } } ) if (areaData.every((d) => !!d)) { markAreas.push(areaData) } } }) return { ...(markLines.length && { markLine: { data: markLines } }), ...(markAreas.length && { markArea: { data: markAreas } }) } } } function getReferenceDataOptions( metricData: number[], valueType: ReferenceValueType, value: any, axis: string ) { const option: any = {} if (valueType === ReferenceValueType.Constant) { option[axis] = value } else { option[axis] = calcAggregateReferenceData(valueType, metricData) } return option } function getReferenceDataMetricAxis( chartType: ChartTypes, options?: { barChart?: boolean metrics?: IWidgetMetric[] metric?: string } ) { switch (chartType) { case ChartTypes.Bar: return options.barChart ? 'xAxis' : 'yAxis' case ChartTypes.Scatter: const axisIndexMapping = ['xAxis', 'yAxis'] const metricIndex = options.metrics.findIndex( (m) => m.name === options.metric ) return axisIndexMapping[metricIndex] default: return 'yAxis' } } function calcAggregateReferenceData( valueType: ReferenceValueType, metricData: number[] ) { switch (valueType) { case ReferenceValueType.Max: return Math.max(...metricData) case ReferenceValueType.Min: return Math.min(...metricData) case ReferenceValueType.Average: return mean(metricData) } }