map.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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 { EChartOption } from 'echarts'
  22. import {
  23. decodeMetricName,
  24. getTextWidth,
  25. getSizeRate
  26. } from '../../components/util'
  27. import {
  28. getLegendOption,
  29. getLabelOption,
  30. getSymbolSize
  31. } from './util'
  32. import {
  33. safeAddition
  34. } from 'utils/util'
  35. import {
  36. DEFAULT_ECHARTS_THEME
  37. } from 'app/globalConstants'
  38. import geoData from 'assets/js/geo.js'
  39. import { getFormattedValue } from '../../components/Config/Format'
  40. const provinceSuffix = ['省', '自治区', '市']
  41. const citySuffix = ['自治州', '市', '区', '县', '旗', '盟', '镇']
  42. export default function (chartProps: IChartProps) {
  43. const {
  44. chartStyles,
  45. data,
  46. cols,
  47. metrics,
  48. model
  49. } = chartProps
  50. const {
  51. label,
  52. spec
  53. } = chartStyles
  54. const {
  55. labelColor,
  56. labelFontFamily,
  57. labelFontSize,
  58. labelPosition,
  59. showLabel
  60. } = label
  61. const {
  62. layerType,
  63. roam,
  64. linesSpeed,
  65. symbolType
  66. } = spec
  67. const tooltip: EChartOption.Tooltip = {
  68. trigger: 'item',
  69. formatter: (params: EChartOption.Tooltip.Format) => {
  70. const { name, data, color } = params
  71. const tooltipLabels = []
  72. if (color) {
  73. tooltipLabels.push(`<span class="widget-tooltip-circle" style="background: ${color}"></span>`)
  74. }
  75. tooltipLabels.push(name)
  76. if (data) {
  77. tooltipLabels.push(': ')
  78. tooltipLabels.push(getFormattedValue(data.value[2], metrics[0].format))
  79. }
  80. return tooltipLabels.join('')
  81. }
  82. }
  83. const labelOption = {
  84. label: {
  85. normal: {
  86. formatter: '{b}',
  87. position: labelPosition,
  88. show: showLabel,
  89. color: labelColor,
  90. fontFamily: labelFontFamily,
  91. fontSize: labelFontSize
  92. }
  93. }
  94. }
  95. const labelOptionLines = {
  96. label: getLabelOption('lines', label, metrics, true)
  97. }
  98. let metricOptions
  99. let visualMapOptions
  100. const dataTree = {}
  101. let min = 0
  102. let max = 0
  103. const agg = metrics[0].agg
  104. const metricName = decodeMetricName(metrics[0].name)
  105. data.forEach((record) => {
  106. let areaVal
  107. const group = []
  108. const value = record[`${agg}(${metricName})`]
  109. min = Math.min(min, value)
  110. max = Math.max(max, value)
  111. cols.forEach((col) => {
  112. const { visualType } = model[col.name]
  113. if (visualType === 'geoProvince') {
  114. areaVal = record[col.name]
  115. const area = getProvinceArea(areaVal)
  116. const provinceName = getProvinceName(areaVal)
  117. if (area) {
  118. if (!dataTree[provinceName]) {
  119. dataTree[provinceName] = {
  120. lon: area.lon,
  121. lat: area.lat,
  122. value,
  123. children: {}
  124. }
  125. }
  126. }
  127. } else if (visualType === 'geoCity') {
  128. areaVal = record[col.name]
  129. const area = getCityArea(areaVal)
  130. if (area) {
  131. if (layerType === 'map') {
  132. const provinceParent = getProvinceParent(area)
  133. const parentName = getProvinceName(provinceParent.name)
  134. if (!dataTree[parentName]) {
  135. dataTree[parentName] = {
  136. lon: area.lon,
  137. lat: area.lat,
  138. value: 0,
  139. children: {}
  140. }
  141. }
  142. dataTree[parentName].value += value
  143. } else {
  144. if (!dataTree[areaVal]) {
  145. dataTree[areaVal] = {
  146. lon: area.lon,
  147. lat: area.lat,
  148. value,
  149. children: {}
  150. }
  151. }
  152. }
  153. }
  154. }
  155. // todo: 除去显示城市/省的
  156. // const group = ['name', 'sex']
  157. // if (group.length) {
  158. // group.forEach((g) => {
  159. // if (!dataTree[areaVal].children[record[g]]) {
  160. // dataTree[areaVal].children[record[g]] = 0
  161. // }
  162. // dataTree[areaVal].children[record[g]] = safeAddition(dataTree[areaVal].children[record[g]], Number(value))
  163. // })
  164. // }
  165. })
  166. })
  167. // series 数据项
  168. const metricArr = []
  169. const sizeRate = getSizeRate(min, max)
  170. const optionsType = layerType === 'scatter' ? {} : {
  171. blurSize: 40
  172. }
  173. let serieObj
  174. switch (layerType) {
  175. case 'map':
  176. serieObj = {
  177. name: '地图',
  178. type: 'map',
  179. mapType: 'china',
  180. roam,
  181. data: Object.keys(dataTree).map((key, index) => {
  182. const { lon, lat, value } = dataTree[key]
  183. return {
  184. name: key,
  185. value: [lon, lat, value]
  186. }
  187. }),
  188. ...labelOption
  189. }
  190. break
  191. case 'scatter':
  192. serieObj = {
  193. name: '气泡图',
  194. type: 'scatter',
  195. coordinateSystem: 'geo',
  196. data: Object.keys(dataTree).map((key, index) => {
  197. const { lon, lat, value } = dataTree[key]
  198. return {
  199. name: key,
  200. value: [lon, lat, value],
  201. symbolSize: getSymbolSize(sizeRate, value) / 2
  202. }
  203. }),
  204. ...labelOption,
  205. ...optionsType
  206. }
  207. break
  208. case 'heatmap':
  209. serieObj = {
  210. name: '热力图',
  211. type: 'heatmap',
  212. coordinateSystem: 'geo',
  213. data: Object.keys(dataTree).map((key, index) => {
  214. const { lon, lat, value } = dataTree[key]
  215. return {
  216. name: key,
  217. value: [lon, lat, value],
  218. symbolSize: getSymbolSize(sizeRate, value) / 2
  219. }
  220. }),
  221. ...labelOption,
  222. ...optionsType
  223. }
  224. break
  225. }
  226. metricArr.push(serieObj)
  227. metricOptions = {
  228. series: metricArr
  229. }
  230. if (chartStyles.visualMap) {
  231. const {
  232. showVisualMap,
  233. visualMapPosition,
  234. fontFamily,
  235. fontSize,
  236. visualMapDirection,
  237. visualMapWidth,
  238. visualMapHeight,
  239. startColor,
  240. endColor
  241. } = chartStyles.visualMap
  242. visualMapOptions = {
  243. visualMap: {
  244. show: layerType === 'lines' ? false : showVisualMap,
  245. min,
  246. max,
  247. calculable: true,
  248. inRange: {
  249. color: [startColor, endColor]
  250. },
  251. ...getPosition(visualMapPosition),
  252. itemWidth: visualMapWidth,
  253. itemHeight: visualMapHeight,
  254. textStyle: {
  255. fontFamily,
  256. fontSize
  257. },
  258. orient: visualMapDirection
  259. }
  260. }
  261. } else {
  262. visualMapOptions = {
  263. visualMap: {
  264. show: false,
  265. min,
  266. max,
  267. calculable: true,
  268. inRange: {
  269. color: DEFAULT_ECHARTS_THEME.visualMapColor
  270. },
  271. left: 10,
  272. bottom: 20,
  273. itemWidth: 20,
  274. itemHeight: 50,
  275. textStyle: {
  276. fontFamily: 'PingFang SC',
  277. fontSize: 12
  278. },
  279. orient: 'vertical'
  280. }
  281. }
  282. }
  283. const getGeoCity = cols.filter((c) => model[c.name].visualType === 'geoCity')
  284. const getGeoProvince = cols.filter((c) => model[c.name].visualType === 'geoProvince')
  285. const linesSeries = []
  286. const legendData = []
  287. let effectScatterType
  288. let linesType
  289. data.forEach((d, index) => {
  290. let linesSeriesData = []
  291. let scatterData = []
  292. const value = d[`${agg}(${metricName})`]
  293. if (getGeoCity.length > 1 && d[getGeoCity[0].name] && d[getGeoCity[1].name]) {
  294. const fromCityInfo = getCityArea(d[getGeoCity[0].name])
  295. const toCityInfo = getCityArea(d[getGeoCity[1].name])
  296. if (fromCityInfo && toCityInfo) {
  297. legendData.push(d[getGeoCity[0].name])
  298. linesSeriesData = [{
  299. fromName: d[getGeoCity[0].name],
  300. toName: d[getGeoCity[1].name],
  301. coords: [[fromCityInfo.lon, fromCityInfo.lat], [toCityInfo.lon, toCityInfo.lat]]
  302. }]
  303. scatterData = [{
  304. name: d[getGeoCity[1].name],
  305. value: [toCityInfo.lon, toCityInfo.lat, value]
  306. }]
  307. }
  308. } else if (getGeoProvince.length > 1 && d[getGeoProvince[0].name] && d[getGeoProvince[1].name]) {
  309. const fromProvinceInfo = getProvinceArea(d[getGeoProvince[0].name])
  310. const toProvinceInfo = getProvinceArea(d[getGeoProvince[1].name])
  311. if (fromProvinceInfo && toProvinceInfo) {
  312. legendData.push(d[getGeoProvince[0].name])
  313. linesSeriesData = [{
  314. fromName: d[getGeoProvince[0].name],
  315. toName: d[getGeoProvince[1].name],
  316. coords: [[fromProvinceInfo.lon, fromProvinceInfo.lat], [toProvinceInfo.lon, toProvinceInfo.lat]]
  317. }]
  318. scatterData = [{
  319. name: d[getGeoProvince[1].name],
  320. value: [toProvinceInfo.lon, toProvinceInfo.lat, value]
  321. }]
  322. }
  323. } else {
  324. return
  325. }
  326. effectScatterType = {
  327. name: '',
  328. type: 'effectScatter',
  329. coordinateSystem: 'geo',
  330. zlevel: index,
  331. rippleEffect: {
  332. brushType: 'stroke'
  333. },
  334. ...labelOptionLines,
  335. symbolSize: (val) => {
  336. return 12
  337. },
  338. data: scatterData
  339. }
  340. linesType = {
  341. name: '',
  342. type: 'lines',
  343. zlevel: index,
  344. symbol: ['none', 'arrow'],
  345. symbolSize: 10,
  346. effect: {
  347. show: true,
  348. // period: 600,
  349. trailLength: 0,
  350. symbol: symbolType,
  351. symbolSize: 15,
  352. constantSpeed: linesSpeed
  353. },
  354. lineStyle: {
  355. normal: {
  356. width: 2,
  357. opacity: 0.6,
  358. curveness: 0.2
  359. }
  360. },
  361. data: linesSeriesData
  362. }
  363. linesSeries.push(linesType, effectScatterType)
  364. })
  365. let legendOption
  366. if (chartStyles.legend) {
  367. const {
  368. color,
  369. fontFamily,
  370. fontSize,
  371. legendPosition,
  372. selectAll,
  373. showLegend
  374. } = chartStyles.legend
  375. legendOption = {
  376. legend: getLegendOption(chartStyles.legend, legendData)
  377. }
  378. } else {
  379. legendOption = null
  380. }
  381. let mapOptions
  382. switch (layerType) {
  383. case 'map':
  384. mapOptions = {
  385. ...metricOptions,
  386. ...visualMapOptions,
  387. tooltip
  388. }
  389. break
  390. case 'lines':
  391. mapOptions = {
  392. ...legendOption,
  393. geo: {
  394. map: 'china',
  395. roam
  396. },
  397. series: linesSeries,
  398. ...visualMapOptions
  399. }
  400. break
  401. case 'scatter':
  402. mapOptions = {
  403. geo: {
  404. map: 'china',
  405. itemStyle: {
  406. normal: {
  407. areaColor: '#cccccc',
  408. borderColor: '#ffffff',
  409. borderWidth: 1
  410. },
  411. emphasis: {
  412. areaColor: '#bbbbbb'
  413. }
  414. },
  415. roam
  416. },
  417. ...metricOptions,
  418. ...visualMapOptions,
  419. tooltip
  420. }
  421. break
  422. case 'heatmap':
  423. mapOptions = {
  424. geo: {
  425. map: 'china',
  426. itemStyle: {
  427. normal: {
  428. areaColor: '#cccccc',
  429. borderColor: '#ffffff',
  430. borderWidth: 1
  431. },
  432. emphasis: {
  433. areaColor: '#bbbbbb'
  434. }
  435. },
  436. label: {
  437. emphasis: {
  438. show: true
  439. }
  440. },
  441. roam
  442. },
  443. ...metricOptions,
  444. ...visualMapOptions
  445. // ...tooltipOptions
  446. }
  447. break
  448. }
  449. return mapOptions
  450. }
  451. function getProvinceParent (area) {
  452. if (!area.parent) {
  453. return area
  454. }
  455. const parent = geoData.find((g) => g.id === area.parent)
  456. return !parent.parent ? parent : getProvinceParent(parent)
  457. }
  458. function getProvinceName (name) {
  459. provinceSuffix.forEach((ps) => {
  460. if (name.includes(ps)) {
  461. name = name.replace(ps, '')
  462. }
  463. })
  464. return name
  465. }
  466. function getCityArea (name) {
  467. const hasSuffix = citySuffix.some((p) => name.includes(p))
  468. const area = hasSuffix
  469. ? geoData.find((d) => d.name === name)
  470. : geoData.find((d) => d.name.includes(name))
  471. return area
  472. }
  473. function getProvinceArea (name) {
  474. const hasSuffix = provinceSuffix.some((p) => name.includes(p))
  475. const area = hasSuffix
  476. ? geoData.find((d) => d.name === name && !d.parent)
  477. : geoData.find((d) => d.name.includes(name) && !d.parent)
  478. return area
  479. }
  480. function getPosition (position) {
  481. let positionValue
  482. switch (position) {
  483. case 'leftBottom':
  484. positionValue = {
  485. left: 'left',
  486. top: 'bottom'
  487. }
  488. break
  489. case 'leftTop':
  490. positionValue = {
  491. left: 'left',
  492. top: 'top'
  493. }
  494. break
  495. case 'rightTop':
  496. positionValue = {
  497. left: 'right',
  498. top: 'top'
  499. }
  500. break
  501. case 'rightBottom':
  502. positionValue = {
  503. left: 'right',
  504. top: 'bottom'
  505. }
  506. break
  507. }
  508. return positionValue
  509. }