ScrollableLegendView.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. var zrUtil = require("zrender/lib/core/util");
  20. var graphic = require("../../util/graphic");
  21. var layoutUtil = require("../../util/layout");
  22. var LegendView = require("./LegendView");
  23. /*
  24. * Licensed to the Apache Software Foundation (ASF) under one
  25. * or more contributor license agreements. See the NOTICE file
  26. * distributed with this work for additional information
  27. * regarding copyright ownership. The ASF licenses this file
  28. * to you under the Apache License, Version 2.0 (the
  29. * "License"); you may not use this file except in compliance
  30. * with the License. You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing,
  35. * software distributed under the License is distributed on an
  36. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  37. * KIND, either express or implied. See the License for the
  38. * specific language governing permissions and limitations
  39. * under the License.
  40. */
  41. /**
  42. * Separate legend and scrollable legend to reduce package size.
  43. */
  44. var Group = graphic.Group;
  45. var WH = ['width', 'height'];
  46. var XY = ['x', 'y'];
  47. var ScrollableLegendView = LegendView.extend({
  48. type: 'legend.scroll',
  49. newlineDisabled: true,
  50. init: function () {
  51. ScrollableLegendView.superCall(this, 'init');
  52. /**
  53. * @private
  54. * @type {number} For `scroll`.
  55. */
  56. this._currentIndex = 0;
  57. /**
  58. * @private
  59. * @type {module:zrender/container/Group}
  60. */
  61. this.group.add(this._containerGroup = new Group());
  62. this._containerGroup.add(this.getContentGroup());
  63. /**
  64. * @private
  65. * @type {module:zrender/container/Group}
  66. */
  67. this.group.add(this._controllerGroup = new Group());
  68. /**
  69. *
  70. * @private
  71. */
  72. this._showController;
  73. },
  74. /**
  75. * @override
  76. */
  77. resetInner: function () {
  78. ScrollableLegendView.superCall(this, 'resetInner');
  79. this._controllerGroup.removeAll();
  80. this._containerGroup.removeClipPath();
  81. this._containerGroup.__rectSize = null;
  82. },
  83. /**
  84. * @override
  85. */
  86. renderInner: function (itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition) {
  87. var me = this; // Render content items.
  88. ScrollableLegendView.superCall(this, 'renderInner', itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition);
  89. var controllerGroup = this._controllerGroup; // FIXME: support be 'auto' adapt to size number text length,
  90. // e.g., '3/12345' should not overlap with the control arrow button.
  91. var pageIconSize = legendModel.get('pageIconSize', true);
  92. if (!zrUtil.isArray(pageIconSize)) {
  93. pageIconSize = [pageIconSize, pageIconSize];
  94. }
  95. createPageButton('pagePrev', 0);
  96. var pageTextStyleModel = legendModel.getModel('pageTextStyle');
  97. controllerGroup.add(new graphic.Text({
  98. name: 'pageText',
  99. style: {
  100. textFill: pageTextStyleModel.getTextColor(),
  101. font: pageTextStyleModel.getFont(),
  102. textVerticalAlign: 'middle',
  103. textAlign: 'center'
  104. },
  105. silent: true
  106. }));
  107. createPageButton('pageNext', 1);
  108. function createPageButton(name, iconIdx) {
  109. var pageDataIndexName = name + 'DataIndex';
  110. var icon = graphic.createIcon(legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx], {
  111. // Buttons will be created in each render, so we do not need
  112. // to worry about avoiding using legendModel kept in scope.
  113. onclick: zrUtil.bind(me._pageGo, me, pageDataIndexName, legendModel, api)
  114. }, {
  115. x: -pageIconSize[0] / 2,
  116. y: -pageIconSize[1] / 2,
  117. width: pageIconSize[0],
  118. height: pageIconSize[1]
  119. });
  120. icon.name = name;
  121. controllerGroup.add(icon);
  122. }
  123. },
  124. /**
  125. * @override
  126. */
  127. layoutInner: function (legendModel, itemAlign, maxSize, isFirstRender, selector, selectorPosition) {
  128. var selectorGroup = this.getSelectorGroup();
  129. var orientIdx = legendModel.getOrient().index;
  130. var wh = WH[orientIdx];
  131. var xy = XY[orientIdx];
  132. var hw = WH[1 - orientIdx];
  133. var yx = XY[1 - orientIdx];
  134. selector && layoutUtil.box( // Buttons in selectorGroup always layout horizontally
  135. 'horizontal', selectorGroup, legendModel.get('selectorItemGap', true));
  136. var selectorButtonGap = legendModel.get('selectorButtonGap', true);
  137. var selectorRect = selectorGroup.getBoundingRect();
  138. var selectorPos = [-selectorRect.x, -selectorRect.y];
  139. var processMaxSize = zrUtil.clone(maxSize);
  140. selector && (processMaxSize[wh] = maxSize[wh] - selectorRect[wh] - selectorButtonGap);
  141. var mainRect = this._layoutContentAndController(legendModel, isFirstRender, processMaxSize, orientIdx, wh, hw, yx);
  142. if (selector) {
  143. if (selectorPosition === 'end') {
  144. selectorPos[orientIdx] += mainRect[wh] + selectorButtonGap;
  145. } else {
  146. var offset = selectorRect[wh] + selectorButtonGap;
  147. selectorPos[orientIdx] -= offset;
  148. mainRect[xy] -= offset;
  149. }
  150. mainRect[wh] += selectorRect[wh] + selectorButtonGap;
  151. selectorPos[1 - orientIdx] += mainRect[yx] + mainRect[hw] / 2 - selectorRect[hw] / 2;
  152. mainRect[hw] = Math.max(mainRect[hw], selectorRect[hw]);
  153. mainRect[yx] = Math.min(mainRect[yx], selectorRect[yx] + selectorPos[1 - orientIdx]);
  154. selectorGroup.attr('position', selectorPos);
  155. }
  156. return mainRect;
  157. },
  158. _layoutContentAndController: function (legendModel, isFirstRender, maxSize, orientIdx, wh, hw, yx) {
  159. var contentGroup = this.getContentGroup();
  160. var containerGroup = this._containerGroup;
  161. var controllerGroup = this._controllerGroup; // Place items in contentGroup.
  162. layoutUtil.box(legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), !orientIdx ? null : maxSize.width, orientIdx ? null : maxSize.height);
  163. layoutUtil.box( // Buttons in controller are layout always horizontally.
  164. 'horizontal', controllerGroup, legendModel.get('pageButtonItemGap', true));
  165. var contentRect = contentGroup.getBoundingRect();
  166. var controllerRect = controllerGroup.getBoundingRect();
  167. var showController = this._showController = contentRect[wh] > maxSize[wh];
  168. var contentPos = [-contentRect.x, -contentRect.y]; // Remain contentPos when scroll animation perfroming.
  169. // If first rendering, `contentGroup.position` is [0, 0], which
  170. // does not make sense and may cause unexepcted animation if adopted.
  171. if (!isFirstRender) {
  172. contentPos[orientIdx] = contentGroup.position[orientIdx];
  173. } // Layout container group based on 0.
  174. var containerPos = [0, 0];
  175. var controllerPos = [-controllerRect.x, -controllerRect.y];
  176. var pageButtonGap = zrUtil.retrieve2(legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true)); // Place containerGroup and controllerGroup and contentGroup.
  177. if (showController) {
  178. var pageButtonPosition = legendModel.get('pageButtonPosition', true); // controller is on the right / bottom.
  179. if (pageButtonPosition === 'end') {
  180. controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh];
  181. } // controller is on the left / top.
  182. else {
  183. containerPos[orientIdx] += controllerRect[wh] + pageButtonGap;
  184. }
  185. } // Always align controller to content as 'middle'.
  186. controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2;
  187. contentGroup.attr('position', contentPos);
  188. containerGroup.attr('position', containerPos);
  189. controllerGroup.attr('position', controllerPos); // Calculate `mainRect` and set `clipPath`.
  190. // mainRect should not be calculated by `this.group.getBoundingRect()`
  191. // for sake of the overflow.
  192. var mainRect = {
  193. x: 0,
  194. y: 0
  195. }; // Consider content may be overflow (should be clipped).
  196. mainRect[wh] = showController ? maxSize[wh] : contentRect[wh];
  197. mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]); // `containerRect[yx] + containerPos[1 - orientIdx]` is 0.
  198. mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]);
  199. containerGroup.__rectSize = maxSize[wh];
  200. if (showController) {
  201. var clipShape = {
  202. x: 0,
  203. y: 0
  204. };
  205. clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0);
  206. clipShape[hw] = mainRect[hw];
  207. containerGroup.setClipPath(new graphic.Rect({
  208. shape: clipShape
  209. })); // Consider content may be larger than container, container rect
  210. // can not be obtained from `containerGroup.getBoundingRect()`.
  211. containerGroup.__rectSize = clipShape[wh];
  212. } else {
  213. // Do not remove or ignore controller. Keep them set as placeholders.
  214. controllerGroup.eachChild(function (child) {
  215. child.attr({
  216. invisible: true,
  217. silent: true
  218. });
  219. });
  220. } // Content translate animation.
  221. var pageInfo = this._getPageInfo(legendModel);
  222. pageInfo.pageIndex != null && graphic.updateProps(contentGroup, {
  223. position: pageInfo.contentPosition
  224. }, // When switch from "show controller" to "not show controller", view should be
  225. // updated immediately without animation, otherwise causes weird effect.
  226. showController ? legendModel : false);
  227. this._updatePageInfoView(legendModel, pageInfo);
  228. return mainRect;
  229. },
  230. _pageGo: function (to, legendModel, api) {
  231. var scrollDataIndex = this._getPageInfo(legendModel)[to];
  232. scrollDataIndex != null && api.dispatchAction({
  233. type: 'legendScroll',
  234. scrollDataIndex: scrollDataIndex,
  235. legendId: legendModel.id
  236. });
  237. },
  238. _updatePageInfoView: function (legendModel, pageInfo) {
  239. var controllerGroup = this._controllerGroup;
  240. zrUtil.each(['pagePrev', 'pageNext'], function (name) {
  241. var canJump = pageInfo[name + 'DataIndex'] != null;
  242. var icon = controllerGroup.childOfName(name);
  243. if (icon) {
  244. icon.setStyle('fill', canJump ? legendModel.get('pageIconColor', true) : legendModel.get('pageIconInactiveColor', true));
  245. icon.cursor = canJump ? 'pointer' : 'default';
  246. }
  247. });
  248. var pageText = controllerGroup.childOfName('pageText');
  249. var pageFormatter = legendModel.get('pageFormatter');
  250. var pageIndex = pageInfo.pageIndex;
  251. var current = pageIndex != null ? pageIndex + 1 : 0;
  252. var total = pageInfo.pageCount;
  253. pageText && pageFormatter && pageText.setStyle('text', zrUtil.isString(pageFormatter) ? pageFormatter.replace('{current}', current).replace('{total}', total) : pageFormatter({
  254. current: current,
  255. total: total
  256. }));
  257. },
  258. /**
  259. * @param {module:echarts/model/Model} legendModel
  260. * @return {Object} {
  261. * contentPosition: Array.<number>, null when data item not found.
  262. * pageIndex: number, null when data item not found.
  263. * pageCount: number, always be a number, can be 0.
  264. * pagePrevDataIndex: number, null when no previous page.
  265. * pageNextDataIndex: number, null when no next page.
  266. * }
  267. */
  268. _getPageInfo: function (legendModel) {
  269. var scrollDataIndex = legendModel.get('scrollDataIndex', true);
  270. var contentGroup = this.getContentGroup();
  271. var containerRectSize = this._containerGroup.__rectSize;
  272. var orientIdx = legendModel.getOrient().index;
  273. var wh = WH[orientIdx];
  274. var xy = XY[orientIdx];
  275. var targetItemIndex = this._findTargetItemIndex(scrollDataIndex);
  276. var children = contentGroup.children();
  277. var targetItem = children[targetItemIndex];
  278. var itemCount = children.length;
  279. var pCount = !itemCount ? 0 : 1;
  280. var result = {
  281. contentPosition: contentGroup.position.slice(),
  282. pageCount: pCount,
  283. pageIndex: pCount - 1,
  284. pagePrevDataIndex: null,
  285. pageNextDataIndex: null
  286. };
  287. if (!targetItem) {
  288. return result;
  289. }
  290. var targetItemInfo = getItemInfo(targetItem);
  291. result.contentPosition[orientIdx] = -targetItemInfo.s; // Strategy:
  292. // (1) Always align based on the left/top most item.
  293. // (2) It is user-friendly that the last item shown in the
  294. // current window is shown at the begining of next window.
  295. // Otherwise if half of the last item is cut by the window,
  296. // it will have no chance to display entirely.
  297. // (3) Consider that item size probably be different, we
  298. // have calculate pageIndex by size rather than item index,
  299. // and we can not get page index directly by division.
  300. // (4) The window is to narrow to contain more than
  301. // one item, we should make sure that the page can be fliped.
  302. for (var i = targetItemIndex + 1, winStartItemInfo = targetItemInfo, winEndItemInfo = targetItemInfo, currItemInfo = null; i <= itemCount; ++i) {
  303. currItemInfo = getItemInfo(children[i]);
  304. if ( // Half of the last item is out of the window.
  305. !currItemInfo && winEndItemInfo.e > winStartItemInfo.s + containerRectSize || // If the current item does not intersect with the window, the new page
  306. // can be started at the current item or the last item.
  307. currItemInfo && !intersect(currItemInfo, winStartItemInfo.s)) {
  308. if (winEndItemInfo.i > winStartItemInfo.i) {
  309. winStartItemInfo = winEndItemInfo;
  310. } else {
  311. // e.g., when page size is smaller than item size.
  312. winStartItemInfo = currItemInfo;
  313. }
  314. if (winStartItemInfo) {
  315. if (result.pageNextDataIndex == null) {
  316. result.pageNextDataIndex = winStartItemInfo.i;
  317. }
  318. ++result.pageCount;
  319. }
  320. }
  321. winEndItemInfo = currItemInfo;
  322. }
  323. for (var i = targetItemIndex - 1, winStartItemInfo = targetItemInfo, winEndItemInfo = targetItemInfo, currItemInfo = null; i >= -1; --i) {
  324. currItemInfo = getItemInfo(children[i]);
  325. if ( // If the the end item does not intersect with the window started
  326. // from the current item, a page can be settled.
  327. (!currItemInfo || !intersect(winEndItemInfo, currItemInfo.s)) && // e.g., when page size is smaller than item size.
  328. winStartItemInfo.i < winEndItemInfo.i) {
  329. winEndItemInfo = winStartItemInfo;
  330. if (result.pagePrevDataIndex == null) {
  331. result.pagePrevDataIndex = winStartItemInfo.i;
  332. }
  333. ++result.pageCount;
  334. ++result.pageIndex;
  335. }
  336. winStartItemInfo = currItemInfo;
  337. }
  338. return result;
  339. function getItemInfo(el) {
  340. if (el) {
  341. var itemRect = el.getBoundingRect();
  342. var start = itemRect[xy] + el.position[orientIdx];
  343. return {
  344. s: start,
  345. e: start + itemRect[wh],
  346. i: el.__legendDataIndex
  347. };
  348. }
  349. }
  350. function intersect(itemInfo, winStart) {
  351. return itemInfo.e >= winStart && itemInfo.s <= winStart + containerRectSize;
  352. }
  353. },
  354. _findTargetItemIndex: function (targetDataIndex) {
  355. if (!this._showController) {
  356. return 0;
  357. }
  358. var index;
  359. var contentGroup = this.getContentGroup();
  360. var defaultIndex;
  361. contentGroup.eachChild(function (child, idx) {
  362. var legendDataIdx = child.__legendDataIndex; // FIXME
  363. // If the given targetDataIndex (from model) is illegal,
  364. // we use defaultIndex. But the index on the legend model and
  365. // action payload is still illegal. That case will not be
  366. // changed until some scenario requires.
  367. if (defaultIndex == null && legendDataIdx != null) {
  368. defaultIndex = idx;
  369. }
  370. if (legendDataIdx === targetDataIndex) {
  371. index = idx;
  372. }
  373. });
  374. return index != null ? index : defaultIndex;
  375. }
  376. });
  377. var _default = ScrollableLegendView;
  378. module.exports = _default;