/* * << * 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 { IChartProps } from '../../components/Chart' import { decodeMetricName, getTextWidth } from '../../components/util' import { getLegendOption, getLabelOption } from './util' import { EChartOption } from 'echarts' import { getFormattedValue } from '../../components/Config/Format' import defaultTheme from 'assets/json/echartsThemes/default.project.json' const defaultThemeColors = defaultTheme.theme.color export default function (chartProps: IChartProps, drillOptions?: any) { const { width, height, data, cols, metrics, chartStyles, color, tip } = chartProps const { label, legend, spec, toolbox } = chartStyles const { legendPosition, fontSize } = legend const { alignmentMode, gapNumber, sortMode } = spec const labelOption = { label: getLabelOption('funnel', label, metrics) } const { selectedItems } = drillOptions let seriesObj = {} const seriesArr = [] const legendData = [] let grouped: { [key: string]: object[] } = {} if (metrics.length <= 1) { const groupColumns = color.items .map((c) => c.name) .concat(cols.map((c) => c.name)) .reduce((distinctColumns, col) => { if (!distinctColumns.includes(col)) { distinctColumns.push(col) } return distinctColumns }, []) grouped = data.reduce<{ [key: string]: object[] }>((obj, val) => { const groupingKey = groupColumns .reduce((keyArr, col) => keyArr.concat(val[col]), []) .join(String.fromCharCode(0)) if (!obj[groupingKey]) { obj[groupingKey] = [] } obj[groupingKey].push(val) return obj }, {}) metrics.forEach((metric) => { const decodedMetricName = decodeMetricName(metric.name) const metricNameWithAgg = `${metric.agg}(${decodedMetricName})` const seriesData = [] Object.entries(grouped).forEach(([key, value]) => { const legendStr = key.replace(String.fromCharCode(0), ' ') legendData.push(legendStr) value.forEach((v) => { const obj = { name: legendStr, value: v[metricNameWithAgg] } seriesData.push(obj) }) }) const maxValue = Math.max( ...data.map((s) => s[metricNameWithAgg]) ) const minValue = Math.min( ...data.map((s) => s[metricNameWithAgg]) ) const numValueArr = data.map( (d) => d[metricNameWithAgg] >= 0 ) const minSizePer = (minValue / maxValue) * 100 const minSizeValue = numValueArr.indexOf(false) === -1 ? `${minSizePer}%` : '0%' const funnelLeft = 56 + Math.max(...legendData.map((s) => getTextWidth(s, '', `${fontSize}px`))) const leftValue = legendPosition === 'left' ? width * 0.15 + funnelLeft : width * 0.15 const topValue = legendPosition === 'top' ? height * 0.12 + 32 : height * 0.12 const heightValue = legendPosition === 'left' || legendPosition === 'right' ? height - height * 0.12 * 2 : height - 32 - height * 0.12 * 2 const widthValue = legendPosition === 'left' || legendPosition === 'right' ? width - funnelLeft - width * 0.15 * 2 : width - width * 0.15 * 2 seriesObj = { name: '', type: 'funnel', min: minValue, max: maxValue, minSize: minSizeValue, maxSize: '100%', sort: sortMode, funnelAlign: alignmentMode, gap: gapNumber || 0, left: leftValue, top: topValue, width: widthValue, height: heightValue, data: getFunnelSeriesData(seriesData) .map((data, index) => { return { ...data, itemStyle: { normal: { ...color.items.length && { color: color.items[0].config.values[data.name] }, opacity: selectedItems && selectedItems.length ? selectedItems.includes(index) ? 1 : 0.25 : 1 } } } }), itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }, ...labelOption } seriesArr.push(seriesObj) }) } else { const seriesData = [] metrics.forEach((metric) => { const decodedMetricName = decodeMetricName(metric.name) legendData.push(decodedMetricName) seriesData.push({ name: decodedMetricName, metricName: metric.name, value: data.reduce((sum, record) => sum + record[`${metric.agg}(${decodedMetricName})`], 0) }) }) seriesObj = { type: 'funnel', sort: sortMode, funnelAlign: alignmentMode, gap: gapNumber || 0, left: width * 0.15, top: height * 0.12, width: width - width * 0.15 * 2, height: height - height * 0.12 * 2, data: getFunnelSeriesData(seriesData) .map((data, index) => ({ ...data, itemStyle: { normal: { color: color.value[data.metricName] || defaultThemeColors[index % defaultThemeColors.length], opacity: selectedItems && selectedItems.length ? selectedItems.includes(index) ? 1 : 0.25 : 1 } } })), itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }, ...labelOption } seriesArr.push(seriesObj) } const tooltip: EChartOption.Tooltip = { trigger: 'item', formatter (params: EChartOption.Tooltip.Format) { const { color, name, value, percent, dataIndex, data } = params const formattedValue = getFormattedValue( value as number, metrics[metrics.length > 1 ? dataIndex : 0].format ) const tooltipLabels = [] let basicInfo = `${name}: ${formattedValue}` if (color) { basicInfo = ` ${basicInfo}` } tooltipLabels.push(basicInfo) if (data.conversion) { tooltipLabels.push(`转化率: ${data.conversion}%`) } if (data.arrival) { tooltipLabels.push(`到达率: ${data.arrival}%`) } tooltipLabels.push(`百分比: ${percent}%`) return tooltipLabels.join('
') } } return { tooltip, legend: getLegendOption(legend, legendData), series: seriesArr } } function getFunnelSeriesData (seriesData) { return seriesData .sort((d1, d2) => d2.value - d1.value) .map((d, index) => { if (index) { d.conversion = formatPercent(d.value / seriesData[index - 1].value * 100) d.arrival = formatPercent(d.value / seriesData[0].value * 100) } return d }) } function formatPercent (per) { const perStr = per + '' return perStr.length - (perStr.indexOf('.') + 1) > 2 ? per.toFixed(2) : perStr }