graphic.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  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 _config = require("../config");
  20. var __DEV__ = _config.__DEV__;
  21. var echarts = require("../echarts");
  22. var zrUtil = require("zrender/lib/core/util");
  23. var modelUtil = require("../util/model");
  24. var graphicUtil = require("../util/graphic");
  25. var layoutUtil = require("../util/layout");
  26. var _number = require("../util/number");
  27. var parsePercent = _number.parsePercent;
  28. /*
  29. * Licensed to the Apache Software Foundation (ASF) under one
  30. * or more contributor license agreements. See the NOTICE file
  31. * distributed with this work for additional information
  32. * regarding copyright ownership. The ASF licenses this file
  33. * to you under the Apache License, Version 2.0 (the
  34. * "License"); you may not use this file except in compliance
  35. * with the License. You may obtain a copy of the License at
  36. *
  37. * http://www.apache.org/licenses/LICENSE-2.0
  38. *
  39. * Unless required by applicable law or agreed to in writing,
  40. * software distributed under the License is distributed on an
  41. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  42. * KIND, either express or implied. See the License for the
  43. * specific language governing permissions and limitations
  44. * under the License.
  45. */
  46. var _nonShapeGraphicElements = {
  47. // Reserved but not supported in graphic component.
  48. path: null,
  49. compoundPath: null,
  50. // Supported in graphic component.
  51. group: graphicUtil.Group,
  52. image: graphicUtil.Image,
  53. text: graphicUtil.Text
  54. }; // -------------
  55. // Preprocessor
  56. // -------------
  57. echarts.registerPreprocessor(function (option) {
  58. var graphicOption = option.graphic; // Convert
  59. // {graphic: [{left: 10, type: 'circle'}, ...]}
  60. // or
  61. // {graphic: {left: 10, type: 'circle'}}
  62. // to
  63. // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]}
  64. if (zrUtil.isArray(graphicOption)) {
  65. if (!graphicOption[0] || !graphicOption[0].elements) {
  66. option.graphic = [{
  67. elements: graphicOption
  68. }];
  69. } else {
  70. // Only one graphic instance can be instantiated. (We dont
  71. // want that too many views are created in echarts._viewMap)
  72. option.graphic = [option.graphic[0]];
  73. }
  74. } else if (graphicOption && !graphicOption.elements) {
  75. option.graphic = [{
  76. elements: [graphicOption]
  77. }];
  78. }
  79. }); // ------
  80. // Model
  81. // ------
  82. var GraphicModel = echarts.extendComponentModel({
  83. type: 'graphic',
  84. defaultOption: {
  85. // Extra properties for each elements:
  86. //
  87. // left/right/top/bottom: (like 12, '22%', 'center', default undefined)
  88. // If left/rigth is set, shape.x/shape.cx/position will not be used.
  89. // If top/bottom is set, shape.y/shape.cy/position will not be used.
  90. // This mechanism is useful when you want to position a group/element
  91. // against the right side or the center of this container.
  92. //
  93. // width/height: (can only be pixel value, default 0)
  94. // Only be used to specify contianer(group) size, if needed. And
  95. // can not be percentage value (like '33%'). See the reason in the
  96. // layout algorithm below.
  97. //
  98. // bounding: (enum: 'all' (default) | 'raw')
  99. // Specify how to calculate boundingRect when locating.
  100. // 'all': Get uioned and transformed boundingRect
  101. // from both itself and its descendants.
  102. // This mode simplies confining a group of elements in the bounding
  103. // of their ancester container (e.g., using 'right: 0').
  104. // 'raw': Only use the boundingRect of itself and before transformed.
  105. // This mode is similar to css behavior, which is useful when you
  106. // want an element to be able to overflow its container. (Consider
  107. // a rotated circle needs to be located in a corner.)
  108. // info: custom info. enables user to mount some info on elements and use them
  109. // in event handlers. Update them only when user specified, otherwise, remain.
  110. // Note: elements is always behind its ancestors in this elements array.
  111. elements: [],
  112. parentId: null
  113. },
  114. /**
  115. * Save el options for the sake of the performance (only update modified graphics).
  116. * The order is the same as those in option. (ancesters -> descendants)
  117. *
  118. * @private
  119. * @type {Array.<Object>}
  120. */
  121. _elOptionsToUpdate: null,
  122. /**
  123. * @override
  124. */
  125. mergeOption: function (option) {
  126. // Prevent default merge to elements
  127. var elements = this.option.elements;
  128. this.option.elements = null;
  129. GraphicModel.superApply(this, 'mergeOption', arguments);
  130. this.option.elements = elements;
  131. },
  132. /**
  133. * @override
  134. */
  135. optionUpdated: function (newOption, isInit) {
  136. var thisOption = this.option;
  137. var newList = (isInit ? thisOption : newOption).elements;
  138. var existList = thisOption.elements = isInit ? [] : thisOption.elements;
  139. var flattenedList = [];
  140. this._flatten(newList, flattenedList);
  141. var mappingResult = modelUtil.mappingToExists(existList, flattenedList);
  142. modelUtil.makeIdAndName(mappingResult); // Clear elOptionsToUpdate
  143. var elOptionsToUpdate = this._elOptionsToUpdate = [];
  144. zrUtil.each(mappingResult, function (resultItem, index) {
  145. var newElOption = resultItem.option;
  146. if (!newElOption) {
  147. return;
  148. }
  149. elOptionsToUpdate.push(newElOption);
  150. setKeyInfoToNewElOption(resultItem, newElOption);
  151. mergeNewElOptionToExist(existList, index, newElOption);
  152. setLayoutInfoToExist(existList[index], newElOption);
  153. }, this); // Clean
  154. for (var i = existList.length - 1; i >= 0; i--) {
  155. if (existList[i] == null) {
  156. existList.splice(i, 1);
  157. } else {
  158. // $action should be volatile, otherwise option gotten from
  159. // `getOption` will contain unexpected $action.
  160. delete existList[i].$action;
  161. }
  162. }
  163. },
  164. /**
  165. * Convert
  166. * [{
  167. * type: 'group',
  168. * id: 'xx',
  169. * children: [{type: 'circle'}, {type: 'polygon'}]
  170. * }]
  171. * to
  172. * [
  173. * {type: 'group', id: 'xx'},
  174. * {type: 'circle', parentId: 'xx'},
  175. * {type: 'polygon', parentId: 'xx'}
  176. * ]
  177. *
  178. * @private
  179. * @param {Array.<Object>} optionList option list
  180. * @param {Array.<Object>} result result of flatten
  181. * @param {Object} parentOption parent option
  182. */
  183. _flatten: function (optionList, result, parentOption) {
  184. zrUtil.each(optionList, function (option) {
  185. if (!option) {
  186. return;
  187. }
  188. if (parentOption) {
  189. option.parentOption = parentOption;
  190. }
  191. result.push(option);
  192. var children = option.children;
  193. if (option.type === 'group' && children) {
  194. this._flatten(children, result, option);
  195. } // Deleting for JSON output, and for not affecting group creation.
  196. delete option.children;
  197. }, this);
  198. },
  199. // FIXME
  200. // Pass to view using payload? setOption has a payload?
  201. useElOptionsToUpdate: function () {
  202. var els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming.
  203. this._elOptionsToUpdate = null;
  204. return els;
  205. }
  206. }); // -----
  207. // View
  208. // -----
  209. echarts.extendComponentView({
  210. type: 'graphic',
  211. /**
  212. * @override
  213. */
  214. init: function (ecModel, api) {
  215. /**
  216. * @private
  217. * @type {module:zrender/core/util.HashMap}
  218. */
  219. this._elMap = zrUtil.createHashMap();
  220. /**
  221. * @private
  222. * @type {module:echarts/graphic/GraphicModel}
  223. */
  224. this._lastGraphicModel;
  225. },
  226. /**
  227. * @override
  228. */
  229. render: function (graphicModel, ecModel, api) {
  230. // Having leveraged between use cases and algorithm complexity, a very
  231. // simple layout mechanism is used:
  232. // The size(width/height) can be determined by itself or its parent (not
  233. // implemented yet), but can not by its children. (Top-down travel)
  234. // The location(x/y) can be determined by the bounding rect of itself
  235. // (can including its descendants or not) and the size of its parent.
  236. // (Bottom-up travel)
  237. // When `chart.clear()` or `chart.setOption({...}, true)` with the same id,
  238. // view will be reused.
  239. if (graphicModel !== this._lastGraphicModel) {
  240. this._clear();
  241. }
  242. this._lastGraphicModel = graphicModel;
  243. this._updateElements(graphicModel);
  244. this._relocate(graphicModel, api);
  245. },
  246. /**
  247. * Update graphic elements.
  248. *
  249. * @private
  250. * @param {Object} graphicModel graphic model
  251. */
  252. _updateElements: function (graphicModel) {
  253. var elOptionsToUpdate = graphicModel.useElOptionsToUpdate();
  254. if (!elOptionsToUpdate) {
  255. return;
  256. }
  257. var elMap = this._elMap;
  258. var rootGroup = this.group; // Top-down tranverse to assign graphic settings to each elements.
  259. zrUtil.each(elOptionsToUpdate, function (elOption) {
  260. var $action = elOption.$action;
  261. var id = elOption.id;
  262. var existEl = elMap.get(id);
  263. var parentId = elOption.parentId;
  264. var targetElParent = parentId != null ? elMap.get(parentId) : rootGroup;
  265. var elOptionStyle = elOption.style;
  266. if (elOption.type === 'text' && elOptionStyle) {
  267. // In top/bottom mode, textVerticalAlign should not be used, which cause
  268. // inaccurately locating.
  269. if (elOption.hv && elOption.hv[1]) {
  270. elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null;
  271. } // Compatible with previous setting: both support fill and textFill,
  272. // stroke and textStroke.
  273. !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && (elOptionStyle.textFill = elOptionStyle.fill);
  274. !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && (elOptionStyle.textStroke = elOptionStyle.stroke);
  275. } // Remove unnecessary props to avoid potential problems.
  276. var elOptionCleaned = getCleanedElOption(elOption); // For simple, do not support parent change, otherwise reorder is needed.
  277. if (!$action || $action === 'merge') {
  278. existEl ? existEl.attr(elOptionCleaned) : createEl(id, targetElParent, elOptionCleaned, elMap);
  279. } else if ($action === 'replace') {
  280. removeEl(existEl, elMap);
  281. createEl(id, targetElParent, elOptionCleaned, elMap);
  282. } else if ($action === 'remove') {
  283. removeEl(existEl, elMap);
  284. }
  285. var el = elMap.get(id);
  286. if (el) {
  287. el.__ecGraphicWidthOption = elOption.width;
  288. el.__ecGraphicHeightOption = elOption.height;
  289. setEventData(el, graphicModel, elOption);
  290. }
  291. });
  292. },
  293. /**
  294. * Locate graphic elements.
  295. *
  296. * @private
  297. * @param {Object} graphicModel graphic model
  298. * @param {module:echarts/ExtensionAPI} api extension API
  299. */
  300. _relocate: function (graphicModel, api) {
  301. var elOptions = graphicModel.option.elements;
  302. var rootGroup = this.group;
  303. var elMap = this._elMap;
  304. var apiWidth = api.getWidth();
  305. var apiHeight = api.getHeight(); // Top-down to calculate percentage width/height of group
  306. for (var i = 0; i < elOptions.length; i++) {
  307. var elOption = elOptions[i];
  308. var el = elMap.get(elOption.id);
  309. if (!el || !el.isGroup) {
  310. continue;
  311. }
  312. var parentEl = el.parent;
  313. var isParentRoot = parentEl === rootGroup; // Like 'position:absolut' in css, default 0.
  314. el.__ecGraphicWidth = parsePercent(el.__ecGraphicWidthOption, isParentRoot ? apiWidth : parentEl.__ecGraphicWidth) || 0;
  315. el.__ecGraphicHeight = parsePercent(el.__ecGraphicHeightOption, isParentRoot ? apiHeight : parentEl.__ecGraphicHeight) || 0;
  316. } // Bottom-up tranvese all elements (consider ec resize) to locate elements.
  317. for (var i = elOptions.length - 1; i >= 0; i--) {
  318. var elOption = elOptions[i];
  319. var el = elMap.get(elOption.id);
  320. if (!el) {
  321. continue;
  322. }
  323. var parentEl = el.parent;
  324. var containerInfo = parentEl === rootGroup ? {
  325. width: apiWidth,
  326. height: apiHeight
  327. } : {
  328. width: parentEl.__ecGraphicWidth,
  329. height: parentEl.__ecGraphicHeight
  330. }; // PENDING
  331. // Currently, when `bounding: 'all'`, the union bounding rect of the group
  332. // does not include the rect of [0, 0, group.width, group.height], which
  333. // is probably weird for users. Should we make a break change for it?
  334. layoutUtil.positionElement(el, elOption, containerInfo, null, {
  335. hv: elOption.hv,
  336. boundingMode: elOption.bounding
  337. });
  338. }
  339. },
  340. /**
  341. * Clear all elements.
  342. *
  343. * @private
  344. */
  345. _clear: function () {
  346. var elMap = this._elMap;
  347. elMap.each(function (el) {
  348. removeEl(el, elMap);
  349. });
  350. this._elMap = zrUtil.createHashMap();
  351. },
  352. /**
  353. * @override
  354. */
  355. dispose: function () {
  356. this._clear();
  357. }
  358. });
  359. function createEl(id, targetElParent, elOption, elMap) {
  360. var graphicType = elOption.type;
  361. var Clz = _nonShapeGraphicElements.hasOwnProperty(graphicType) // Those graphic elements are not shapes. They should not be
  362. // overwritten by users, so do them first.
  363. ? _nonShapeGraphicElements[graphicType] : graphicUtil.getShapeClass(graphicType);
  364. var el = new Clz(elOption);
  365. targetElParent.add(el);
  366. elMap.set(id, el);
  367. el.__ecGraphicId = id;
  368. }
  369. function removeEl(existEl, elMap) {
  370. var existElParent = existEl && existEl.parent;
  371. if (existElParent) {
  372. existEl.type === 'group' && existEl.traverse(function (el) {
  373. removeEl(el, elMap);
  374. });
  375. elMap.removeKey(existEl.__ecGraphicId);
  376. existElParent.remove(existEl);
  377. }
  378. } // Remove unnecessary props to avoid potential problems.
  379. function getCleanedElOption(elOption) {
  380. elOption = zrUtil.extend({}, elOption);
  381. zrUtil.each(['id', 'parentId', '$action', 'hv', 'bounding'].concat(layoutUtil.LOCATION_PARAMS), function (name) {
  382. delete elOption[name];
  383. });
  384. return elOption;
  385. }
  386. function isSetLoc(obj, props) {
  387. var isSet;
  388. zrUtil.each(props, function (prop) {
  389. obj[prop] != null && obj[prop] !== 'auto' && (isSet = true);
  390. });
  391. return isSet;
  392. }
  393. function setKeyInfoToNewElOption(resultItem, newElOption) {
  394. var existElOption = resultItem.exist; // Set id and type after id assigned.
  395. newElOption.id = resultItem.keyInfo.id;
  396. !newElOption.type && existElOption && (newElOption.type = existElOption.type); // Set parent id if not specified
  397. if (newElOption.parentId == null) {
  398. var newElParentOption = newElOption.parentOption;
  399. if (newElParentOption) {
  400. newElOption.parentId = newElParentOption.id;
  401. } else if (existElOption) {
  402. newElOption.parentId = existElOption.parentId;
  403. }
  404. } // Clear
  405. newElOption.parentOption = null;
  406. }
  407. function mergeNewElOptionToExist(existList, index, newElOption) {
  408. // Update existing options, for `getOption` feature.
  409. var newElOptCopy = zrUtil.extend({}, newElOption);
  410. var existElOption = existList[index];
  411. var $action = newElOption.$action || 'merge';
  412. if ($action === 'merge') {
  413. if (existElOption) {
  414. // We can ensure that newElOptCopy and existElOption are not
  415. // the same object, so `merge` will not change newElOptCopy.
  416. zrUtil.merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize.
  417. layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, {
  418. ignoreSize: true
  419. }); // Will be used in render.
  420. layoutUtil.copyLayoutParams(newElOption, existElOption);
  421. } else {
  422. existList[index] = newElOptCopy;
  423. }
  424. } else if ($action === 'replace') {
  425. existList[index] = newElOptCopy;
  426. } else if ($action === 'remove') {
  427. // null will be cleaned later.
  428. existElOption && (existList[index] = null);
  429. }
  430. }
  431. function setLayoutInfoToExist(existItem, newElOption) {
  432. if (!existItem) {
  433. return;
  434. }
  435. existItem.hv = newElOption.hv = [// Rigid body, dont care `width`.
  436. isSetLoc(newElOption, ['left', 'right']), // Rigid body, dont care `height`.
  437. isSetLoc(newElOption, ['top', 'bottom'])]; // Give default group size. Otherwise layout error may occur.
  438. if (existItem.type === 'group') {
  439. existItem.width == null && (existItem.width = newElOption.width = 0);
  440. existItem.height == null && (existItem.height = newElOption.height = 0);
  441. }
  442. }
  443. function setEventData(el, graphicModel, elOption) {
  444. var eventData = el.eventData; // Simple optimize for large amount of elements that no need event.
  445. if (!el.silent && !el.ignore && !eventData) {
  446. eventData = el.eventData = {
  447. componentType: 'graphic',
  448. componentIndex: graphicModel.componentIndex,
  449. name: el.name
  450. };
  451. } // `elOption.info` enables user to mount some info on
  452. // elements and use them in event handlers.
  453. if (eventData) {
  454. eventData.info = el.info;
  455. }
  456. }