funnel.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. define('echarts/chart/funnel', [
  2. 'require',
  3. './base',
  4. 'zrender/shape/Text',
  5. 'zrender/shape/Line',
  6. 'zrender/shape/Polygon',
  7. '../config',
  8. '../util/ecData',
  9. '../util/number',
  10. 'zrender/tool/util',
  11. 'zrender/tool/color',
  12. 'zrender/tool/area',
  13. '../chart'
  14. ], function (require) {
  15. var ChartBase = require('./base');
  16. var TextShape = require('zrender/shape/Text');
  17. var LineShape = require('zrender/shape/Line');
  18. var PolygonShape = require('zrender/shape/Polygon');
  19. var ecConfig = require('../config');
  20. ecConfig.funnel = {
  21. zlevel: 0,
  22. z: 2,
  23. clickable: true,
  24. legendHoverLink: true,
  25. x: 80,
  26. y: 60,
  27. x2: 80,
  28. y2: 60,
  29. min: 0,
  30. max: 100,
  31. minSize: '0%',
  32. maxSize: '100%',
  33. sort: 'descending',
  34. gap: 0,
  35. funnelAlign: 'center',
  36. itemStyle: {
  37. normal: {
  38. borderColor: '#fff',
  39. borderWidth: 1,
  40. label: {
  41. show: true,
  42. position: 'outer'
  43. },
  44. labelLine: {
  45. show: true,
  46. length: 10,
  47. lineStyle: {
  48. width: 1,
  49. type: 'solid'
  50. }
  51. }
  52. },
  53. emphasis: {
  54. borderColor: 'rgba(0,0,0,0)',
  55. borderWidth: 1,
  56. label: { show: true },
  57. labelLine: { show: true }
  58. }
  59. }
  60. };
  61. var ecData = require('../util/ecData');
  62. var number = require('../util/number');
  63. var zrUtil = require('zrender/tool/util');
  64. var zrColor = require('zrender/tool/color');
  65. var zrArea = require('zrender/tool/area');
  66. function Funnel(ecTheme, messageCenter, zr, option, myChart) {
  67. ChartBase.call(this, ecTheme, messageCenter, zr, option, myChart);
  68. this.refresh(option);
  69. }
  70. Funnel.prototype = {
  71. type: ecConfig.CHART_TYPE_FUNNEL,
  72. _buildShape: function () {
  73. var series = this.series;
  74. var legend = this.component.legend;
  75. this._paramsMap = {};
  76. this._selected = {};
  77. this.selectedMap = {};
  78. var serieName;
  79. for (var i = 0, l = series.length; i < l; i++) {
  80. if (series[i].type === ecConfig.CHART_TYPE_FUNNEL) {
  81. series[i] = this.reformOption(series[i]);
  82. this.legendHoverLink = series[i].legendHoverLink || this.legendHoverLink;
  83. serieName = series[i].name || '';
  84. this.selectedMap[serieName] = legend ? legend.isSelected(serieName) : true;
  85. if (!this.selectedMap[serieName]) {
  86. continue;
  87. }
  88. this._buildSingleFunnel(i);
  89. this.buildMark(i);
  90. }
  91. }
  92. this.addShapeList();
  93. },
  94. _buildSingleFunnel: function (seriesIndex) {
  95. var legend = this.component.legend;
  96. var serie = this.series[seriesIndex];
  97. var data = this._mapData(seriesIndex);
  98. var location = this._getLocation(seriesIndex);
  99. this._paramsMap[seriesIndex] = {
  100. location: location,
  101. data: data
  102. };
  103. var itemName;
  104. var total = 0;
  105. var selectedData = [];
  106. for (var i = 0, l = data.length; i < l; i++) {
  107. itemName = data[i].name;
  108. this.selectedMap[itemName] = legend ? legend.isSelected(itemName) : true;
  109. if (this.selectedMap[itemName] && !isNaN(data[i].value)) {
  110. selectedData.push(data[i]);
  111. total++;
  112. }
  113. }
  114. if (total === 0) {
  115. return;
  116. }
  117. var funnelCase = this._buildFunnelCase(seriesIndex);
  118. var align = serie.funnelAlign;
  119. var gap = serie.gap;
  120. var height = total > 1 ? (location.height - (total - 1) * gap) / total : location.height;
  121. var width;
  122. var lastY = location.y;
  123. var lastWidth = serie.sort === 'descending' ? this._getItemWidth(seriesIndex, selectedData[0].value) : number.parsePercent(serie.minSize, location.width);
  124. var next = serie.sort === 'descending' ? 1 : 0;
  125. var centerX = location.centerX;
  126. var pointList = [];
  127. var x;
  128. var polygon;
  129. var lastPolygon;
  130. for (var i = 0, l = selectedData.length; i < l; i++) {
  131. itemName = selectedData[i].name;
  132. if (this.selectedMap[itemName] && !isNaN(selectedData[i].value)) {
  133. width = i <= l - 2 ? this._getItemWidth(seriesIndex, selectedData[i + next].value) : serie.sort === 'descending' ? number.parsePercent(serie.minSize, location.width) : number.parsePercent(serie.maxSize, location.width);
  134. switch (align) {
  135. case 'left':
  136. x = location.x;
  137. break;
  138. case 'right':
  139. x = location.x + location.width - lastWidth;
  140. break;
  141. default:
  142. x = centerX - lastWidth / 2;
  143. }
  144. polygon = this._buildItem(seriesIndex, selectedData[i]._index, legend ? legend.getColor(itemName) : this.zr.getColor(selectedData[i]._index), x, lastY, lastWidth, width, height, align);
  145. lastY += height + gap;
  146. lastPolygon = polygon.style.pointList;
  147. pointList.unshift([
  148. lastPolygon[0][0] - 10,
  149. lastPolygon[0][1]
  150. ]);
  151. pointList.push([
  152. lastPolygon[1][0] + 10,
  153. lastPolygon[1][1]
  154. ]);
  155. if (i === 0) {
  156. if (lastWidth === 0) {
  157. lastPolygon = pointList.pop();
  158. align == 'center' && (pointList[0][0] += 10);
  159. align == 'right' && (pointList[0][0] = lastPolygon[0]);
  160. pointList[0][1] -= align == 'center' ? 10 : 15;
  161. if (l == 1) {
  162. lastPolygon = polygon.style.pointList;
  163. }
  164. } else {
  165. pointList[pointList.length - 1][1] -= 5;
  166. pointList[0][1] -= 5;
  167. }
  168. }
  169. lastWidth = width;
  170. }
  171. }
  172. if (funnelCase) {
  173. pointList.unshift([
  174. lastPolygon[3][0] - 10,
  175. lastPolygon[3][1]
  176. ]);
  177. pointList.push([
  178. lastPolygon[2][0] + 10,
  179. lastPolygon[2][1]
  180. ]);
  181. if (lastWidth === 0) {
  182. lastPolygon = pointList.pop();
  183. align == 'center' && (pointList[0][0] += 10);
  184. align == 'right' && (pointList[0][0] = lastPolygon[0]);
  185. pointList[0][1] += align == 'center' ? 10 : 15;
  186. } else {
  187. pointList[pointList.length - 1][1] += 5;
  188. pointList[0][1] += 5;
  189. }
  190. funnelCase.style.pointList = pointList;
  191. }
  192. },
  193. _buildFunnelCase: function (seriesIndex) {
  194. var serie = this.series[seriesIndex];
  195. if (this.deepQuery([
  196. serie,
  197. this.option
  198. ], 'calculable')) {
  199. var location = this._paramsMap[seriesIndex].location;
  200. var gap = 10;
  201. var funnelCase = {
  202. hoverable: false,
  203. style: {
  204. pointListd: [
  205. [
  206. location.x - gap,
  207. location.y - gap
  208. ],
  209. [
  210. location.x + location.width + gap,
  211. location.y - gap
  212. ],
  213. [
  214. location.x + location.width + gap,
  215. location.y + location.height + gap
  216. ],
  217. [
  218. location.x - gap,
  219. location.y + location.height + gap
  220. ]
  221. ],
  222. brushType: 'stroke',
  223. lineWidth: 1,
  224. strokeColor: serie.calculableHolderColor || this.ecTheme.calculableHolderColor || ecConfig.calculableHolderColor
  225. }
  226. };
  227. ecData.pack(funnelCase, serie, seriesIndex, undefined, -1);
  228. this.setCalculable(funnelCase);
  229. funnelCase = new PolygonShape(funnelCase);
  230. this.shapeList.push(funnelCase);
  231. return funnelCase;
  232. }
  233. },
  234. _getLocation: function (seriesIndex) {
  235. var gridOption = this.series[seriesIndex];
  236. var zrWidth = this.zr.getWidth();
  237. var zrHeight = this.zr.getHeight();
  238. var x = this.parsePercent(gridOption.x, zrWidth);
  239. var y = this.parsePercent(gridOption.y, zrHeight);
  240. var width = gridOption.width == null ? zrWidth - x - this.parsePercent(gridOption.x2, zrWidth) : this.parsePercent(gridOption.width, zrWidth);
  241. return {
  242. x: x,
  243. y: y,
  244. width: width,
  245. height: gridOption.height == null ? zrHeight - y - this.parsePercent(gridOption.y2, zrHeight) : this.parsePercent(gridOption.height, zrHeight),
  246. centerX: x + width / 2
  247. };
  248. },
  249. _mapData: function (seriesIndex) {
  250. var serie = this.series[seriesIndex];
  251. var funnelData = zrUtil.clone(serie.data);
  252. for (var i = 0, l = funnelData.length; i < l; i++) {
  253. funnelData[i]._index = i;
  254. }
  255. function numDescending(a, b) {
  256. if (a.value === '-') {
  257. return 1;
  258. } else if (b.value === '-') {
  259. return -1;
  260. }
  261. return b.value - a.value;
  262. }
  263. function numAscending(a, b) {
  264. return -numDescending(a, b);
  265. }
  266. if (serie.sort != 'none') {
  267. funnelData.sort(serie.sort === 'descending' ? numDescending : numAscending);
  268. }
  269. return funnelData;
  270. },
  271. _buildItem: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
  272. var series = this.series;
  273. var serie = series[seriesIndex];
  274. var data = serie.data[dataIndex];
  275. var polygon = this.getPolygon(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
  276. ecData.pack(polygon, series[seriesIndex], seriesIndex, series[seriesIndex].data[dataIndex], dataIndex, series[seriesIndex].data[dataIndex].name);
  277. this.shapeList.push(polygon);
  278. var label = this.getLabel(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
  279. ecData.pack(label, series[seriesIndex], seriesIndex, series[seriesIndex].data[dataIndex], dataIndex, series[seriesIndex].data[dataIndex].name);
  280. this.shapeList.push(label);
  281. if (!this._needLabel(serie, data, false)) {
  282. label.invisible = true;
  283. }
  284. var labelLine = this.getLabelLine(seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align);
  285. this.shapeList.push(labelLine);
  286. if (!this._needLabelLine(serie, data, false)) {
  287. labelLine.invisible = true;
  288. }
  289. var polygonHoverConnect = [];
  290. var labelHoverConnect = [];
  291. if (this._needLabelLine(serie, data, true)) {
  292. polygonHoverConnect.push(labelLine.id);
  293. labelHoverConnect.push(labelLine.id);
  294. }
  295. if (this._needLabel(serie, data, true)) {
  296. polygonHoverConnect.push(label.id);
  297. labelHoverConnect.push(polygon.id);
  298. }
  299. polygon.hoverConnect = polygonHoverConnect;
  300. label.hoverConnect = labelHoverConnect;
  301. return polygon;
  302. },
  303. _getItemWidth: function (seriesIndex, value) {
  304. var serie = this.series[seriesIndex];
  305. var location = this._paramsMap[seriesIndex].location;
  306. var min = serie.min;
  307. var max = serie.max;
  308. var minSize = number.parsePercent(serie.minSize, location.width);
  309. var maxSize = number.parsePercent(serie.maxSize, location.width);
  310. return (value - min) * (maxSize - minSize) / (max - min) + minSize;
  311. },
  312. getPolygon: function (seriesIndex, dataIndex, defaultColor, xLT, y, topWidth, bottomWidth, height, align) {
  313. var serie = this.series[seriesIndex];
  314. var data = serie.data[dataIndex];
  315. var queryTarget = [
  316. data,
  317. serie
  318. ];
  319. var normal = this.deepMerge(queryTarget, 'itemStyle.normal') || {};
  320. var emphasis = this.deepMerge(queryTarget, 'itemStyle.emphasis') || {};
  321. var normalColor = this.getItemStyleColor(normal.color, seriesIndex, dataIndex, data) || defaultColor;
  322. var emphasisColor = this.getItemStyleColor(emphasis.color, seriesIndex, dataIndex, data) || (typeof normalColor === 'string' ? zrColor.lift(normalColor, -0.2) : normalColor);
  323. var xLB;
  324. switch (align) {
  325. case 'left':
  326. xLB = xLT;
  327. break;
  328. case 'right':
  329. xLB = xLT + (topWidth - bottomWidth);
  330. break;
  331. default:
  332. xLB = xLT + (topWidth - bottomWidth) / 2;
  333. break;
  334. }
  335. var polygon = {
  336. zlevel: this.getZlevelBase(),
  337. z: this.getZBase(),
  338. clickable: this.deepQuery(queryTarget, 'clickable'),
  339. style: {
  340. pointList: [
  341. [
  342. xLT,
  343. y
  344. ],
  345. [
  346. xLT + topWidth,
  347. y
  348. ],
  349. [
  350. xLB + bottomWidth,
  351. y + height
  352. ],
  353. [
  354. xLB,
  355. y + height
  356. ]
  357. ],
  358. brushType: 'both',
  359. color: normalColor,
  360. lineWidth: normal.borderWidth,
  361. strokeColor: normal.borderColor
  362. },
  363. highlightStyle: {
  364. color: emphasisColor,
  365. lineWidth: emphasis.borderWidth,
  366. strokeColor: emphasis.borderColor
  367. }
  368. };
  369. if (this.deepQuery([
  370. data,
  371. serie,
  372. this.option
  373. ], 'calculable')) {
  374. this.setCalculable(polygon);
  375. polygon.draggable = true;
  376. }
  377. return new PolygonShape(polygon);
  378. },
  379. getLabel: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
  380. var serie = this.series[seriesIndex];
  381. var data = serie.data[dataIndex];
  382. var location = this._paramsMap[seriesIndex].location;
  383. var itemStyle = zrUtil.merge(zrUtil.clone(data.itemStyle) || {}, serie.itemStyle);
  384. var status = 'normal';
  385. var labelControl = itemStyle[status].label;
  386. var textStyle = labelControl.textStyle || {};
  387. var lineLength = itemStyle[status].labelLine.length;
  388. var text = this.getLabelText(seriesIndex, dataIndex, status);
  389. var textFont = this.getFont(textStyle);
  390. var textAlign;
  391. var textColor = defaultColor;
  392. labelControl.position = labelControl.position || itemStyle.normal.label.position;
  393. if (labelControl.position === 'inner' || labelControl.position === 'inside' || labelControl.position === 'center') {
  394. textAlign = align;
  395. textColor = Math.max(topWidth, bottomWidth) / 2 > zrArea.getTextWidth(text, textFont) ? '#fff' : zrColor.reverse(defaultColor);
  396. } else if (labelControl.position === 'left') {
  397. textAlign = 'right';
  398. } else {
  399. textAlign = 'left';
  400. }
  401. var textShape = {
  402. zlevel: this.getZlevelBase(),
  403. z: this.getZBase() + 1,
  404. style: {
  405. x: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
  406. y: y + height / 2,
  407. color: textStyle.color || textColor,
  408. text: text,
  409. textAlign: textStyle.align || textAlign,
  410. textBaseline: textStyle.baseline || 'middle',
  411. textFont: textFont
  412. }
  413. };
  414. status = 'emphasis';
  415. labelControl = itemStyle[status].label || labelControl;
  416. textStyle = labelControl.textStyle || textStyle;
  417. lineLength = itemStyle[status].labelLine.length || lineLength;
  418. labelControl.position = labelControl.position || itemStyle.normal.label.position;
  419. text = this.getLabelText(seriesIndex, dataIndex, status);
  420. textFont = this.getFont(textStyle);
  421. textColor = defaultColor;
  422. if (labelControl.position === 'inner' || labelControl.position === 'inside' || labelControl.position === 'center') {
  423. textAlign = align;
  424. textColor = Math.max(topWidth, bottomWidth) / 2 > zrArea.getTextWidth(text, textFont) ? '#fff' : zrColor.reverse(defaultColor);
  425. } else if (labelControl.position === 'left') {
  426. textAlign = 'right';
  427. } else {
  428. textAlign = 'left';
  429. }
  430. textShape.highlightStyle = {
  431. x: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
  432. color: textStyle.color || textColor,
  433. text: text,
  434. textAlign: textStyle.align || textAlign,
  435. textFont: textFont,
  436. brushType: 'fill'
  437. };
  438. return new TextShape(textShape);
  439. },
  440. getLabelText: function (seriesIndex, dataIndex, status) {
  441. var series = this.series;
  442. var serie = series[seriesIndex];
  443. var data = serie.data[dataIndex];
  444. var formatter = this.deepQuery([
  445. data,
  446. serie
  447. ], 'itemStyle.' + status + '.label.formatter');
  448. if (formatter) {
  449. if (typeof formatter === 'function') {
  450. return formatter.call(this.myChart, {
  451. seriesIndex: seriesIndex,
  452. seriesName: serie.name || '',
  453. series: serie,
  454. dataIndex: dataIndex,
  455. data: data,
  456. name: data.name,
  457. value: data.value
  458. });
  459. } else if (typeof formatter === 'string') {
  460. formatter = formatter.replace('{a}', '{a0}').replace('{b}', '{b0}').replace('{c}', '{c0}').replace('{a0}', serie.name).replace('{b0}', data.name).replace('{c0}', data.value);
  461. return formatter;
  462. }
  463. } else {
  464. return data.name;
  465. }
  466. },
  467. getLabelLine: function (seriesIndex, dataIndex, defaultColor, x, y, topWidth, bottomWidth, height, align) {
  468. var serie = this.series[seriesIndex];
  469. var data = serie.data[dataIndex];
  470. var location = this._paramsMap[seriesIndex].location;
  471. var itemStyle = zrUtil.merge(zrUtil.clone(data.itemStyle) || {}, serie.itemStyle);
  472. var status = 'normal';
  473. var labelLineControl = itemStyle[status].labelLine;
  474. var lineLength = itemStyle[status].labelLine.length;
  475. var lineStyle = labelLineControl.lineStyle || {};
  476. var labelControl = itemStyle[status].label;
  477. labelControl.position = labelControl.position || itemStyle.normal.label.position;
  478. var lineShape = {
  479. zlevel: this.getZlevelBase(),
  480. z: this.getZBase() + 1,
  481. hoverable: false,
  482. style: {
  483. xStart: this._getLabelLineStartPoint(x, location, topWidth, bottomWidth, align),
  484. yStart: y + height / 2,
  485. xEnd: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
  486. yEnd: y + height / 2,
  487. strokeColor: lineStyle.color || defaultColor,
  488. lineType: lineStyle.type,
  489. lineWidth: lineStyle.width
  490. }
  491. };
  492. status = 'emphasis';
  493. labelLineControl = itemStyle[status].labelLine || labelLineControl;
  494. lineLength = itemStyle[status].labelLine.length || lineLength;
  495. lineStyle = labelLineControl.lineStyle || lineStyle;
  496. labelControl = itemStyle[status].label || labelControl;
  497. labelControl.position = labelControl.position;
  498. lineShape.highlightStyle = {
  499. xEnd: this._getLabelPoint(labelControl.position, x, location, topWidth, bottomWidth, lineLength, align),
  500. strokeColor: lineStyle.color || defaultColor,
  501. lineType: lineStyle.type,
  502. lineWidth: lineStyle.width
  503. };
  504. return new LineShape(lineShape);
  505. },
  506. _getLabelPoint: function (position, x, location, topWidth, bottomWidth, lineLength, align) {
  507. position = position === 'inner' || position === 'inside' ? 'center' : position;
  508. switch (position) {
  509. case 'center':
  510. return align == 'center' ? x + topWidth / 2 : align == 'left' ? x + 10 : x + topWidth - 10;
  511. case 'left':
  512. if (lineLength === 'auto') {
  513. return location.x - 10;
  514. } else {
  515. return align == 'center' ? location.centerX - Math.max(topWidth, bottomWidth) / 2 - lineLength : align == 'right' ? x - (topWidth < bottomWidth ? bottomWidth - topWidth : 0) - lineLength : location.x - lineLength;
  516. }
  517. break;
  518. default:
  519. if (lineLength === 'auto') {
  520. return location.x + location.width + 10;
  521. } else {
  522. return align == 'center' ? location.centerX + Math.max(topWidth, bottomWidth) / 2 + lineLength : align == 'right' ? location.x + location.width + lineLength : x + Math.max(topWidth, bottomWidth) + lineLength;
  523. }
  524. }
  525. },
  526. _getLabelLineStartPoint: function (x, location, topWidth, bottomWidth, align) {
  527. return align == 'center' ? location.centerX : topWidth < bottomWidth ? x + Math.min(topWidth, bottomWidth) / 2 : x + Math.max(topWidth, bottomWidth) / 2;
  528. },
  529. _needLabel: function (serie, data, isEmphasis) {
  530. return this.deepQuery([
  531. data,
  532. serie
  533. ], 'itemStyle.' + (isEmphasis ? 'emphasis' : 'normal') + '.label.show');
  534. },
  535. _needLabelLine: function (serie, data, isEmphasis) {
  536. return this.deepQuery([
  537. data,
  538. serie
  539. ], 'itemStyle.' + (isEmphasis ? 'emphasis' : 'normal') + '.labelLine.show');
  540. },
  541. refresh: function (newOption) {
  542. if (newOption) {
  543. this.option = newOption;
  544. this.series = newOption.series;
  545. }
  546. this.backupShapeList();
  547. this._buildShape();
  548. }
  549. };
  550. zrUtil.inherits(Funnel, ChartBase);
  551. require('../chart').define('funnel', Funnel);
  552. return Funnel;
  553. });