funnel.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import { IChartProps } from '../../components/Chart'
  21. import {
  22. decodeMetricName,
  23. getTextWidth
  24. } from '../../components/util'
  25. import { getLegendOption, getLabelOption } from './util'
  26. import { EChartOption } from 'echarts'
  27. import { getFormattedValue } from '../../components/Config/Format'
  28. import defaultTheme from 'assets/json/echartsThemes/default.project.json'
  29. const defaultThemeColors = defaultTheme.theme.color
  30. export default function (chartProps: IChartProps, drillOptions?: any) {
  31. const {
  32. width,
  33. height,
  34. data,
  35. cols,
  36. metrics,
  37. chartStyles,
  38. color,
  39. tip
  40. } = chartProps
  41. const { label, legend, spec, toolbox } = chartStyles
  42. const { legendPosition, fontSize } = legend
  43. const { alignmentMode, gapNumber, sortMode } = spec
  44. const labelOption = {
  45. label: getLabelOption('funnel', label, metrics)
  46. }
  47. const { selectedItems } = drillOptions
  48. let seriesObj = {}
  49. const seriesArr = []
  50. const legendData = []
  51. let grouped: { [key: string]: object[] } = {}
  52. if (metrics.length <= 1) {
  53. const groupColumns = color.items
  54. .map((c) => c.name)
  55. .concat(cols.map((c) => c.name))
  56. .reduce((distinctColumns, col) => {
  57. if (!distinctColumns.includes(col)) {
  58. distinctColumns.push(col)
  59. }
  60. return distinctColumns
  61. }, [])
  62. grouped = data.reduce<{ [key: string]: object[] }>((obj, val) => {
  63. const groupingKey = groupColumns
  64. .reduce((keyArr, col) => keyArr.concat(val[col]), [])
  65. .join(String.fromCharCode(0))
  66. if (!obj[groupingKey]) {
  67. obj[groupingKey] = []
  68. }
  69. obj[groupingKey].push(val)
  70. return obj
  71. }, {})
  72. metrics.forEach((metric) => {
  73. const decodedMetricName = decodeMetricName(metric.name)
  74. const metricNameWithAgg = `${metric.agg}(${decodedMetricName})`
  75. const seriesData = []
  76. Object.entries(grouped).forEach(([key, value]) => {
  77. const legendStr = key.replace(String.fromCharCode(0), ' ')
  78. legendData.push(legendStr)
  79. value.forEach((v) => {
  80. const obj = {
  81. name: legendStr,
  82. value: v[metricNameWithAgg]
  83. }
  84. seriesData.push(obj)
  85. })
  86. })
  87. const maxValue = Math.max(
  88. ...data.map((s) => s[metricNameWithAgg])
  89. )
  90. const minValue = Math.min(
  91. ...data.map((s) => s[metricNameWithAgg])
  92. )
  93. const numValueArr = data.map(
  94. (d) => d[metricNameWithAgg] >= 0
  95. )
  96. const minSizePer = (minValue / maxValue) * 100
  97. const minSizeValue =
  98. numValueArr.indexOf(false) === -1 ? `${minSizePer}%` : '0%'
  99. const funnelLeft =
  100. 56 +
  101. Math.max(...legendData.map((s) => getTextWidth(s, '', `${fontSize}px`)))
  102. const leftValue =
  103. legendPosition === 'left' ? width * 0.15 + funnelLeft : width * 0.15
  104. const topValue =
  105. legendPosition === 'top' ? height * 0.12 + 32 : height * 0.12
  106. const heightValue =
  107. legendPosition === 'left' || legendPosition === 'right'
  108. ? height - height * 0.12 * 2
  109. : height - 32 - height * 0.12 * 2
  110. const widthValue =
  111. legendPosition === 'left' || legendPosition === 'right'
  112. ? width - funnelLeft - width * 0.15 * 2
  113. : width - width * 0.15 * 2
  114. seriesObj = {
  115. name: '',
  116. type: 'funnel',
  117. min: minValue,
  118. max: maxValue,
  119. minSize: minSizeValue,
  120. maxSize: '100%',
  121. sort: sortMode,
  122. funnelAlign: alignmentMode,
  123. gap: gapNumber || 0,
  124. left: leftValue,
  125. top: topValue,
  126. width: widthValue,
  127. height: heightValue,
  128. data: getFunnelSeriesData(seriesData)
  129. .map((data, index) => {
  130. return {
  131. ...data,
  132. itemStyle: {
  133. normal: {
  134. ...color.items.length && {
  135. color: color.items[0].config.values[data.name]
  136. },
  137. opacity: selectedItems && selectedItems.length
  138. ? selectedItems.includes(index) ? 1 : 0.25
  139. : 1
  140. }
  141. }
  142. }
  143. }),
  144. itemStyle: {
  145. emphasis: {
  146. shadowBlur: 10,
  147. shadowOffsetX: 0,
  148. shadowColor: 'rgba(0, 0, 0, 0.5)'
  149. }
  150. },
  151. ...labelOption
  152. }
  153. seriesArr.push(seriesObj)
  154. })
  155. } else {
  156. const seriesData = []
  157. metrics.forEach((metric) => {
  158. const decodedMetricName = decodeMetricName(metric.name)
  159. legendData.push(decodedMetricName)
  160. seriesData.push({
  161. name: decodedMetricName,
  162. metricName: metric.name,
  163. value: data.reduce((sum, record) => sum + record[`${metric.agg}(${decodedMetricName})`], 0)
  164. })
  165. })
  166. seriesObj = {
  167. type: 'funnel',
  168. sort: sortMode,
  169. funnelAlign: alignmentMode,
  170. gap: gapNumber || 0,
  171. left: width * 0.15,
  172. top: height * 0.12,
  173. width: width - width * 0.15 * 2,
  174. height: height - height * 0.12 * 2,
  175. data: getFunnelSeriesData(seriesData)
  176. .map((data, index) => ({
  177. ...data,
  178. itemStyle: {
  179. normal: {
  180. color: color.value[data.metricName] || defaultThemeColors[index % defaultThemeColors.length],
  181. opacity: selectedItems && selectedItems.length
  182. ? selectedItems.includes(index) ? 1 : 0.25
  183. : 1
  184. }
  185. }
  186. })),
  187. itemStyle: {
  188. emphasis: {
  189. shadowBlur: 10,
  190. shadowOffsetX: 0,
  191. shadowColor: 'rgba(0, 0, 0, 0.5)'
  192. }
  193. },
  194. ...labelOption
  195. }
  196. seriesArr.push(seriesObj)
  197. }
  198. const tooltip: EChartOption.Tooltip = {
  199. trigger: 'item',
  200. formatter (params: EChartOption.Tooltip.Format) {
  201. const { color, name, value, percent, dataIndex, data } = params
  202. const formattedValue = getFormattedValue(
  203. value as number,
  204. metrics[metrics.length > 1 ? dataIndex : 0].format
  205. )
  206. const tooltipLabels = []
  207. let basicInfo = `${name}: ${formattedValue}`
  208. if (color) {
  209. basicInfo = `<span class="widget-tooltip-circle" style="background: ${color}"></span> ${basicInfo}`
  210. }
  211. tooltipLabels.push(basicInfo)
  212. if (data.conversion) {
  213. tooltipLabels.push(`转化率: ${data.conversion}%`)
  214. }
  215. if (data.arrival) {
  216. tooltipLabels.push(`到达率: ${data.arrival}%`)
  217. }
  218. tooltipLabels.push(`百分比: ${percent}%`)
  219. return tooltipLabels.join('<br/>')
  220. }
  221. }
  222. return {
  223. tooltip,
  224. legend: getLegendOption(legend, legendData),
  225. series: seriesArr
  226. }
  227. }
  228. function getFunnelSeriesData (seriesData) {
  229. return seriesData
  230. .sort((d1, d2) => d2.value - d1.value)
  231. .map((d, index) => {
  232. if (index) {
  233. d.conversion = formatPercent(d.value / seriesData[index - 1].value * 100)
  234. d.arrival = formatPercent(d.value / seriesData[0].value * 100)
  235. }
  236. return d
  237. })
  238. }
  239. function formatPercent (per) {
  240. const perStr = per + ''
  241. return perStr.length - (perStr.indexOf('.') + 1) > 2
  242. ? per.toFixed(2)
  243. : perStr
  244. }