legend.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
  2. import { deepMix, mix, each, isNil, isObject, isBoolean, createEvent, parsePadding, isPlainObject, removeEventListener, addEventListener, Array } from '../util/common';
  3. import List from '../component/list';
  4. import Global, { lang } from '../global';
  5. var LEGEND_GAP = 12;
  6. var MARKER_SIZE = 3;
  7. var DEFAULT_CFG = {
  8. itemMarginBottom: 12,
  9. itemGap: 10,
  10. showTitle: false,
  11. titleStyle: {
  12. fontSize: 12,
  13. fill: '#808080',
  14. textAlign: 'start',
  15. textBaseline: 'top'
  16. },
  17. nameStyle: {
  18. fill: '#808080',
  19. fontSize: 12,
  20. textAlign: 'start',
  21. textBaseline: 'middle'
  22. },
  23. valueStyle: {
  24. fill: '#000000',
  25. fontSize: 12,
  26. textAlign: 'start',
  27. textBaseline: 'middle'
  28. },
  29. unCheckStyle: {
  30. fill: '#bfbfbf'
  31. },
  32. itemWidth: 'auto',
  33. wordSpace: 6,
  34. selectedMode: 'multiple' // 'multiple' or 'single'
  35. }; // Register the default configuration for Legend
  36. Global.legend = deepMix({
  37. common: DEFAULT_CFG,
  38. // common legend configuration
  39. right: mix({
  40. position: 'right',
  41. layout: 'vertical'
  42. }, DEFAULT_CFG),
  43. left: mix({
  44. position: 'left',
  45. layout: 'vertical'
  46. }, DEFAULT_CFG),
  47. top: mix({
  48. position: 'top',
  49. layout: 'horizontal'
  50. }, DEFAULT_CFG),
  51. bottom: mix({
  52. position: 'bottom',
  53. layout: 'horizontal'
  54. }, DEFAULT_CFG)
  55. }, Global.legend || {});
  56. function getPaddingByPos(pos, appendPadding) {
  57. var padding = 0;
  58. appendPadding = parsePadding(appendPadding);
  59. switch (pos) {
  60. case 'top':
  61. padding = appendPadding[0];
  62. break;
  63. case 'right':
  64. padding = appendPadding[1];
  65. break;
  66. case 'bottom':
  67. padding = appendPadding[2];
  68. break;
  69. case 'left':
  70. padding = appendPadding[3];
  71. break;
  72. default:
  73. break;
  74. }
  75. return padding;
  76. }
  77. class LegendController {
  78. constructor(cfg) {
  79. var _this = this;
  80. _defineProperty(this, "handleEvent", function (ev) {
  81. var self = _this;
  82. function findItem(x, y) {
  83. var result = null;
  84. var legends = self.legends;
  85. each(legends, function (legendItems) {
  86. each(legendItems, function (legend) {
  87. var {
  88. itemsGroup,
  89. legendHitBoxes
  90. } = legend;
  91. var children = itemsGroup.get('children');
  92. if (children.length) {
  93. var legendPosX = legend.x;
  94. var legendPosY = legend.y;
  95. each(legendHitBoxes, function (box, index) {
  96. if (x >= box.x + legendPosX && x <= box.x + box.width + legendPosX && y >= box.y + legendPosY && y <= box.height + box.y + legendPosY) {
  97. // inbox
  98. result = {
  99. clickedItem: children[index],
  100. clickedLegend: legend
  101. };
  102. return false;
  103. }
  104. });
  105. }
  106. });
  107. });
  108. return result;
  109. }
  110. var chart = self.chart;
  111. var {
  112. x,
  113. y
  114. } = createEvent(ev, chart);
  115. var clicked = findItem(x, y);
  116. if (clicked && clicked.clickedLegend.clickable !== false) {
  117. var {
  118. clickedItem,
  119. clickedLegend
  120. } = clicked;
  121. if (clickedLegend.onClick) {
  122. ev.clickedItem = clickedItem;
  123. clickedLegend.onClick(ev);
  124. } else if (!clickedLegend.custom) {
  125. var checked = clickedItem.get('checked');
  126. var value = clickedItem.get('dataValue');
  127. var {
  128. filteredVals,
  129. field,
  130. selectedMode
  131. } = clickedLegend;
  132. var isSingeSelected = selectedMode === 'single';
  133. if (isSingeSelected) {
  134. chart.filter(field, function (val) {
  135. return val === value;
  136. });
  137. } else {
  138. if (checked) {
  139. filteredVals.push(value);
  140. } else {
  141. Array.remove(filteredVals, value);
  142. }
  143. chart.filter(field, function (val) {
  144. return filteredVals.indexOf(val) === -1;
  145. });
  146. }
  147. chart.repaint();
  148. }
  149. }
  150. });
  151. this.legendCfg = {};
  152. this.enable = true;
  153. this.position = 'top';
  154. mix(this, cfg);
  155. var _chart = this.chart;
  156. this.canvasDom = _chart.get('canvas').get('el');
  157. this.clear();
  158. }
  159. addLegend(scale, items, filteredVals) {
  160. var self = this;
  161. var legendCfg = self.legendCfg;
  162. var field = scale.field;
  163. var fieldCfg = legendCfg[field];
  164. if (fieldCfg === false) {
  165. return null;
  166. }
  167. if (fieldCfg && fieldCfg.custom) {
  168. self.addCustomLegend(field);
  169. } else {
  170. var position = legendCfg.position || self.position;
  171. if (fieldCfg && fieldCfg.position) {
  172. position = fieldCfg.position;
  173. }
  174. if (scale.isCategory) {
  175. self._addCategoryLegend(scale, items, position, filteredVals);
  176. }
  177. }
  178. }
  179. addCustomLegend(field) {
  180. var self = this;
  181. var legendCfg = self.legendCfg;
  182. if (field && legendCfg[field]) {
  183. legendCfg = legendCfg[field];
  184. }
  185. var position = legendCfg.position || self.position;
  186. var legends = self.legends;
  187. legends[position] = legends[position] || [];
  188. var items = legendCfg.items;
  189. if (!items) {
  190. return null;
  191. }
  192. var container = self.container;
  193. each(items, function (item) {
  194. if (!isPlainObject(item.marker)) {
  195. item.marker = {
  196. symbol: item.marker || 'circle',
  197. fill: item.fill,
  198. radius: MARKER_SIZE
  199. };
  200. } else {
  201. item.marker.radius = item.marker.radius || MARKER_SIZE;
  202. }
  203. item.checked = isNil(item.checked) ? true : item.checked;
  204. item.name = item.name || item.value;
  205. });
  206. var legend = new List(deepMix({}, Global.legend[position], legendCfg, {
  207. maxLength: self._getMaxLength(position),
  208. items,
  209. parent: container
  210. }));
  211. legends[position].push(legend);
  212. }
  213. clear() {
  214. var legends = this.legends;
  215. each(legends, function (legendItems) {
  216. each(legendItems, function (legend) {
  217. legend.clear();
  218. });
  219. });
  220. this.legends = {};
  221. this.unBindEvents();
  222. }
  223. _isFiltered(scale, values, value) {
  224. var rst = false;
  225. each(values, function (val) {
  226. rst = rst || scale.getText(val) === scale.getText(value);
  227. if (rst) {
  228. return false;
  229. }
  230. });
  231. return rst;
  232. }
  233. _getMaxLength(position) {
  234. var chart = this.chart;
  235. var appendPadding = parsePadding(chart.get('appendPadding'));
  236. return position === 'right' || position === 'left' ? chart.get('height') - (appendPadding[0] + appendPadding[2]) : chart.get('width') - (appendPadding[1] + appendPadding[3]);
  237. }
  238. _addCategoryLegend(scale, items, position, filteredVals) {
  239. var self = this;
  240. var {
  241. legendCfg,
  242. legends,
  243. container,
  244. chart
  245. } = self;
  246. var field = scale.field;
  247. legends[position] = legends[position] || [];
  248. var symbol = 'circle';
  249. if (legendCfg[field] && legendCfg[field].marker) {
  250. symbol = legendCfg[field].marker;
  251. } else if (legendCfg.marker) {
  252. symbol = legendCfg.marker;
  253. }
  254. each(items, function (item) {
  255. if (isPlainObject(symbol)) {
  256. mix(item.marker, symbol);
  257. } else {
  258. item.marker.symbol = symbol;
  259. }
  260. if (filteredVals) {
  261. item.checked = !self._isFiltered(scale, filteredVals, item.dataValue);
  262. }
  263. });
  264. var legendItems = chart.get('legendItems');
  265. legendItems[field] = items;
  266. var lastCfg = deepMix({}, Global.legend[position], legendCfg[field] || legendCfg, {
  267. maxLength: self._getMaxLength(position),
  268. items,
  269. field,
  270. filteredVals,
  271. parent: container
  272. });
  273. if (lastCfg.showTitle) {
  274. deepMix(lastCfg, {
  275. title: scale.alias || scale.field
  276. });
  277. }
  278. var legend = new List(lastCfg);
  279. legends[position].push(legend);
  280. return legend;
  281. }
  282. _alignLegend(legend, pre, position) {
  283. var self = this;
  284. var {
  285. tl,
  286. bl
  287. } = self.plotRange;
  288. var chart = self.chart;
  289. var offsetX = legend.offsetX || 0;
  290. var offsetY = legend.offsetY || 0;
  291. var chartWidth = chart.get('width');
  292. var chartHeight = chart.get('height');
  293. var appendPadding = parsePadding(chart.get('appendPadding'));
  294. var legendHeight = legend.getHeight();
  295. var legendWidth = legend.getWidth();
  296. var x = 0;
  297. var y = 0;
  298. if (position === 'left' || position === 'right') {
  299. var verticalAlign = legend.verticalAlign || 'middle';
  300. var height = Math.abs(tl.y - bl.y);
  301. x = position === 'left' ? appendPadding[3] : chartWidth - legendWidth - appendPadding[1];
  302. y = (height - legendHeight) / 2 + tl.y;
  303. if (verticalAlign === 'top') {
  304. y = tl.y;
  305. } else if (verticalAlign === 'bottom') {
  306. y = bl.y - legendHeight;
  307. }
  308. if (pre) {
  309. y = pre.get('y') - legendHeight - LEGEND_GAP;
  310. }
  311. } else {
  312. var align = legend.align || 'left';
  313. x = appendPadding[3];
  314. if (align === 'center') {
  315. x = chartWidth / 2 - legendWidth / 2;
  316. } else if (align === 'right') {
  317. x = chartWidth - (legendWidth + appendPadding[1]);
  318. }
  319. y = position === 'top' ? appendPadding[0] + Math.abs(legend.container.getBBox().minY) : chartHeight - legendHeight;
  320. if (pre) {
  321. var preWidth = pre.getWidth();
  322. x = pre.x + preWidth + LEGEND_GAP;
  323. }
  324. }
  325. if (position === 'bottom' && offsetY > 0) {
  326. offsetY = 0;
  327. }
  328. if (position === 'right' && offsetX > 0) {
  329. offsetX = 0;
  330. }
  331. legend.moveTo(x + offsetX, y + offsetY);
  332. }
  333. alignLegends() {
  334. var self = this;
  335. var legends = self.legends;
  336. each(legends, function (legendItems, position) {
  337. each(legendItems, function (legend, index) {
  338. var pre = legendItems[index - 1];
  339. self._alignLegend(legend, pre, position);
  340. });
  341. });
  342. return self;
  343. }
  344. bindEvents() {
  345. var legendCfg = this.legendCfg;
  346. var triggerOn = legendCfg.triggerOn || 'touchstart';
  347. addEventListener(this.canvasDom, triggerOn, this.handleEvent);
  348. }
  349. unBindEvents() {
  350. var legendCfg = this.legendCfg;
  351. var triggerOn = legendCfg.triggerOn || 'touchstart';
  352. removeEventListener(this.canvasDom, triggerOn, this.handleEvent);
  353. }
  354. }
  355. function init(chart) {
  356. var legendController = new LegendController({
  357. container: chart.get('backPlot').addGroup(),
  358. plotRange: chart.get('plotRange'),
  359. chart
  360. });
  361. chart.set('legendController', legendController);
  362. chart.legend = function (field, cfg) {
  363. var legendCfg = legendController.legendCfg;
  364. legendController.enable = true;
  365. if (isBoolean(field)) {
  366. legendController.enable = field;
  367. legendCfg = cfg || {};
  368. } else if (isObject(field)) {
  369. legendCfg = field;
  370. } else {
  371. legendCfg[field] = cfg;
  372. }
  373. legendController.legendCfg = legendCfg;
  374. return this;
  375. };
  376. }
  377. function beforeGeomDraw(chart) {
  378. var legendController = chart.get('legendController');
  379. if (!legendController.enable) return null; // legend is not displayed
  380. var {
  381. legendCfg,
  382. container
  383. } = legendController;
  384. if (legendCfg && legendCfg.custom) {
  385. legendController.addCustomLegend();
  386. } else {
  387. var legendItems = chart.getLegendItems();
  388. var scales = chart.get('scales');
  389. var filters = chart.get('filters');
  390. each(legendItems, function (items, field) {
  391. var scale = scales[field];
  392. var values = scale.values;
  393. var filteredVals;
  394. if (filters && filters[field]) {
  395. filteredVals = values.filter(function (v) {
  396. return !filters[field](v);
  397. });
  398. } else {
  399. filteredVals = [];
  400. }
  401. legendController.addLegend(scale, items, filteredVals);
  402. });
  403. }
  404. if (legendCfg && legendCfg.clickable !== false) {
  405. legendController.bindEvents();
  406. }
  407. var legends = legendController.legends;
  408. var legendRange = {
  409. top: 0,
  410. right: 0,
  411. bottom: 0,
  412. left: 0
  413. };
  414. each(legends, function (legendItems, position) {
  415. var padding = 0;
  416. each(legendItems, function (legend) {
  417. var width = legend.getWidth();
  418. var height = legend.getHeight();
  419. if (position === 'top' || position === 'bottom') {
  420. padding = Math.max(padding, height);
  421. if (legend.offsetY > 0) {
  422. padding += legend.offsetY;
  423. }
  424. } else {
  425. padding = Math.max(padding, width);
  426. if (legend.offsetX > 0) {
  427. padding += legend.offsetX;
  428. }
  429. }
  430. });
  431. legendRange[position] = padding + getPaddingByPos(position, chart.get('appendPadding'));
  432. });
  433. chart.set('legendRange', legendRange);
  434. if (Object.keys(legends).length) {
  435. container.set('ariaLabel', lang.legend.prefix);
  436. } else {
  437. container.set('ariaLabel', null);
  438. }
  439. }
  440. function afterGeomDraw(chart) {
  441. var legendController = chart.get('legendController');
  442. legendController.alignLegends();
  443. }
  444. function clearInner(chart) {
  445. var legendController = chart.get('legendController');
  446. legendController.clear();
  447. chart.set('legendRange', null);
  448. }
  449. export { init, beforeGeomDraw, afterGeomDraw, clearInner };
  450. export default {
  451. init,
  452. beforeGeomDraw,
  453. afterGeomDraw,
  454. clearInner
  455. };