graphic.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. var _config = require("../config");
  2. var __DEV__ = _config.__DEV__;
  3. var echarts = require("../echarts");
  4. var zrUtil = require("zrender/lib/core/util");
  5. var modelUtil = require("../util/model");
  6. var graphicUtil = require("../util/graphic");
  7. var layoutUtil = require("../util/layout");
  8. // -------------
  9. // Preprocessor
  10. // -------------
  11. echarts.registerPreprocessor(function (option) {
  12. var graphicOption = option.graphic; // Convert
  13. // {graphic: [{left: 10, type: 'circle'}, ...]}
  14. // or
  15. // {graphic: {left: 10, type: 'circle'}}
  16. // to
  17. // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]}
  18. if (zrUtil.isArray(graphicOption)) {
  19. if (!graphicOption[0] || !graphicOption[0].elements) {
  20. option.graphic = [{
  21. elements: graphicOption
  22. }];
  23. } else {
  24. // Only one graphic instance can be instantiated. (We dont
  25. // want that too many views are created in echarts._viewMap)
  26. option.graphic = [option.graphic[0]];
  27. }
  28. } else if (graphicOption && !graphicOption.elements) {
  29. option.graphic = [{
  30. elements: [graphicOption]
  31. }];
  32. }
  33. }); // ------
  34. // Model
  35. // ------
  36. var GraphicModel = echarts.extendComponentModel({
  37. type: 'graphic',
  38. defaultOption: {
  39. // Extra properties for each elements:
  40. //
  41. // left/right/top/bottom: (like 12, '22%', 'center', default undefined)
  42. // If left/rigth is set, shape.x/shape.cx/position will not be used.
  43. // If top/bottom is set, shape.y/shape.cy/position will not be used.
  44. // This mechanism is useful when you want to position a group/element
  45. // against the right side or the center of this container.
  46. //
  47. // width/height: (can only be pixel value, default 0)
  48. // Only be used to specify contianer(group) size, if needed. And
  49. // can not be percentage value (like '33%'). See the reason in the
  50. // layout algorithm below.
  51. //
  52. // bounding: (enum: 'all' (default) | 'raw')
  53. // Specify how to calculate boundingRect when locating.
  54. // 'all': Get uioned and transformed boundingRect
  55. // from both itself and its descendants.
  56. // This mode simplies confining a group of elements in the bounding
  57. // of their ancester container (e.g., using 'right: 0').
  58. // 'raw': Only use the boundingRect of itself and before transformed.
  59. // This mode is similar to css behavior, which is useful when you
  60. // want an element to be able to overflow its container. (Consider
  61. // a rotated circle needs to be located in a corner.)
  62. // Note: elements is always behind its ancestors in this elements array.
  63. elements: [],
  64. parentId: null
  65. },
  66. /**
  67. * Save el options for the sake of the performance (only update modified graphics).
  68. * The order is the same as those in option. (ancesters -> descendants)
  69. *
  70. * @private
  71. * @type {Array.<Object>}
  72. */
  73. _elOptionsToUpdate: null,
  74. /**
  75. * @override
  76. */
  77. mergeOption: function (option) {
  78. // Prevent default merge to elements
  79. var elements = this.option.elements;
  80. this.option.elements = null;
  81. GraphicModel.superApply(this, 'mergeOption', arguments);
  82. this.option.elements = elements;
  83. },
  84. /**
  85. * @override
  86. */
  87. optionUpdated: function (newOption, isInit) {
  88. var thisOption = this.option;
  89. var newList = (isInit ? thisOption : newOption).elements;
  90. var existList = thisOption.elements = isInit ? [] : thisOption.elements;
  91. var flattenedList = [];
  92. this._flatten(newList, flattenedList);
  93. var mappingResult = modelUtil.mappingToExists(existList, flattenedList);
  94. modelUtil.makeIdAndName(mappingResult); // Clear elOptionsToUpdate
  95. var elOptionsToUpdate = this._elOptionsToUpdate = [];
  96. zrUtil.each(mappingResult, function (resultItem, index) {
  97. var newElOption = resultItem.option;
  98. if (!newElOption) {
  99. return;
  100. }
  101. elOptionsToUpdate.push(newElOption);
  102. setKeyInfoToNewElOption(resultItem, newElOption);
  103. mergeNewElOptionToExist(existList, index, newElOption);
  104. setLayoutInfoToExist(existList[index], newElOption);
  105. }, this); // Clean
  106. for (var i = existList.length - 1; i >= 0; i--) {
  107. if (existList[i] == null) {
  108. existList.splice(i, 1);
  109. } else {
  110. // $action should be volatile, otherwise option gotten from
  111. // `getOption` will contain unexpected $action.
  112. delete existList[i].$action;
  113. }
  114. }
  115. },
  116. /**
  117. * Convert
  118. * [{
  119. * type: 'group',
  120. * id: 'xx',
  121. * children: [{type: 'circle'}, {type: 'polygon'}]
  122. * }]
  123. * to
  124. * [
  125. * {type: 'group', id: 'xx'},
  126. * {type: 'circle', parentId: 'xx'},
  127. * {type: 'polygon', parentId: 'xx'}
  128. * ]
  129. *
  130. * @private
  131. * @param {Array.<Object>} optionList option list
  132. * @param {Array.<Object>} result result of flatten
  133. * @param {Object} parentOption parent option
  134. */
  135. _flatten: function (optionList, result, parentOption) {
  136. zrUtil.each(optionList, function (option) {
  137. if (!option) {
  138. return;
  139. }
  140. if (parentOption) {
  141. option.parentOption = parentOption;
  142. }
  143. result.push(option);
  144. var children = option.children;
  145. if (option.type === 'group' && children) {
  146. this._flatten(children, result, option);
  147. } // Deleting for JSON output, and for not affecting group creation.
  148. delete option.children;
  149. }, this);
  150. },
  151. // FIXME
  152. // Pass to view using payload? setOption has a payload?
  153. useElOptionsToUpdate: function () {
  154. var els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming.
  155. this._elOptionsToUpdate = null;
  156. return els;
  157. }
  158. }); // -----
  159. // View
  160. // -----
  161. echarts.extendComponentView({
  162. type: 'graphic',
  163. /**
  164. * @override
  165. */
  166. init: function (ecModel, api) {
  167. /**
  168. * @private
  169. * @type {module:zrender/core/util.HashMap}
  170. */
  171. this._elMap = zrUtil.createHashMap();
  172. /**
  173. * @private
  174. * @type {module:echarts/graphic/GraphicModel}
  175. */
  176. this._lastGraphicModel;
  177. },
  178. /**
  179. * @override
  180. */
  181. render: function (graphicModel, ecModel, api) {
  182. // Having leveraged between use cases and algorithm complexity, a very
  183. // simple layout mechanism is used:
  184. // The size(width/height) can be determined by itself or its parent (not
  185. // implemented yet), but can not by its children. (Top-down travel)
  186. // The location(x/y) can be determined by the bounding rect of itself
  187. // (can including its descendants or not) and the size of its parent.
  188. // (Bottom-up travel)
  189. // When `chart.clear()` or `chart.setOption({...}, true)` with the same id,
  190. // view will be reused.
  191. if (graphicModel !== this._lastGraphicModel) {
  192. this._clear();
  193. }
  194. this._lastGraphicModel = graphicModel;
  195. this._updateElements(graphicModel, api);
  196. this._relocate(graphicModel, api);
  197. },
  198. /**
  199. * Update graphic elements.
  200. *
  201. * @private
  202. * @param {Object} graphicModel graphic model
  203. * @param {module:echarts/ExtensionAPI} api extension API
  204. */
  205. _updateElements: function (graphicModel, api) {
  206. var elOptionsToUpdate = graphicModel.useElOptionsToUpdate();
  207. if (!elOptionsToUpdate) {
  208. return;
  209. }
  210. var elMap = this._elMap;
  211. var rootGroup = this.group; // Top-down tranverse to assign graphic settings to each elements.
  212. zrUtil.each(elOptionsToUpdate, function (elOption) {
  213. var $action = elOption.$action;
  214. var id = elOption.id;
  215. var existEl = elMap.get(id);
  216. var parentId = elOption.parentId;
  217. var targetElParent = parentId != null ? elMap.get(parentId) : rootGroup;
  218. if (elOption.type === 'text') {
  219. var elOptionStyle = elOption.style; // In top/bottom mode, textVerticalAlign should not be used, which cause
  220. // inaccurately locating.
  221. if (elOption.hv && elOption.hv[1]) {
  222. elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null;
  223. } // Compatible with previous setting: both support fill and textFill,
  224. // stroke and textStroke.
  225. !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && (elOptionStyle.textFill = elOptionStyle.fill);
  226. !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && (elOptionStyle.textStroke = elOptionStyle.stroke);
  227. } // Remove unnecessary props to avoid potential problems.
  228. var elOptionCleaned = getCleanedElOption(elOption); // For simple, do not support parent change, otherwise reorder is needed.
  229. if (!$action || $action === 'merge') {
  230. existEl ? existEl.attr(elOptionCleaned) : createEl(id, targetElParent, elOptionCleaned, elMap);
  231. } else if ($action === 'replace') {
  232. removeEl(existEl, elMap);
  233. createEl(id, targetElParent, elOptionCleaned, elMap);
  234. } else if ($action === 'remove') {
  235. removeEl(existEl, elMap);
  236. }
  237. var el = elMap.get(id);
  238. if (el) {
  239. el.__ecGraphicWidth = elOption.width;
  240. el.__ecGraphicHeight = elOption.height;
  241. }
  242. });
  243. },
  244. /**
  245. * Locate graphic elements.
  246. *
  247. * @private
  248. * @param {Object} graphicModel graphic model
  249. * @param {module:echarts/ExtensionAPI} api extension API
  250. */
  251. _relocate: function (graphicModel, api) {
  252. var elOptions = graphicModel.option.elements;
  253. var rootGroup = this.group;
  254. var elMap = this._elMap; // Bottom-up tranvese all elements (consider ec resize) to locate elements.
  255. for (var i = elOptions.length - 1; i >= 0; i--) {
  256. var elOption = elOptions[i];
  257. var el = elMap.get(elOption.id);
  258. if (!el) {
  259. continue;
  260. }
  261. var parentEl = el.parent;
  262. var containerInfo = parentEl === rootGroup ? {
  263. width: api.getWidth(),
  264. height: api.getHeight()
  265. } : {
  266. // Like 'position:absolut' in css, default 0.
  267. width: parentEl.__ecGraphicWidth || 0,
  268. height: parentEl.__ecGraphicHeight || 0
  269. };
  270. layoutUtil.positionElement(el, elOption, containerInfo, null, {
  271. hv: elOption.hv,
  272. boundingMode: elOption.bounding
  273. });
  274. }
  275. },
  276. /**
  277. * Clear all elements.
  278. *
  279. * @private
  280. */
  281. _clear: function () {
  282. var elMap = this._elMap;
  283. elMap.each(function (el) {
  284. removeEl(el, elMap);
  285. });
  286. this._elMap = zrUtil.createHashMap();
  287. },
  288. /**
  289. * @override
  290. */
  291. dispose: function () {
  292. this._clear();
  293. }
  294. });
  295. function createEl(id, targetElParent, elOption, elMap) {
  296. var graphicType = elOption.type;
  297. var Clz = graphicUtil[graphicType.charAt(0).toUpperCase() + graphicType.slice(1)];
  298. var el = new Clz(elOption);
  299. targetElParent.add(el);
  300. elMap.set(id, el);
  301. el.__ecGraphicId = id;
  302. }
  303. function removeEl(existEl, elMap) {
  304. var existElParent = existEl && existEl.parent;
  305. if (existElParent) {
  306. existEl.type === 'group' && existEl.traverse(function (el) {
  307. removeEl(el, elMap);
  308. });
  309. elMap.removeKey(existEl.__ecGraphicId);
  310. existElParent.remove(existEl);
  311. }
  312. } // Remove unnecessary props to avoid potential problems.
  313. function getCleanedElOption(elOption) {
  314. elOption = zrUtil.extend({}, elOption);
  315. zrUtil.each(['id', 'parentId', '$action', 'hv', 'bounding'].concat(layoutUtil.LOCATION_PARAMS), function (name) {
  316. delete elOption[name];
  317. });
  318. return elOption;
  319. }
  320. function isSetLoc(obj, props) {
  321. var isSet;
  322. zrUtil.each(props, function (prop) {
  323. obj[prop] != null && obj[prop] !== 'auto' && (isSet = true);
  324. });
  325. return isSet;
  326. }
  327. function setKeyInfoToNewElOption(resultItem, newElOption) {
  328. var existElOption = resultItem.exist; // Set id and type after id assigned.
  329. newElOption.id = resultItem.keyInfo.id;
  330. !newElOption.type && existElOption && (newElOption.type = existElOption.type); // Set parent id if not specified
  331. if (newElOption.parentId == null) {
  332. var newElParentOption = newElOption.parentOption;
  333. if (newElParentOption) {
  334. newElOption.parentId = newElParentOption.id;
  335. } else if (existElOption) {
  336. newElOption.parentId = existElOption.parentId;
  337. }
  338. } // Clear
  339. newElOption.parentOption = null;
  340. }
  341. function mergeNewElOptionToExist(existList, index, newElOption) {
  342. // Update existing options, for `getOption` feature.
  343. var newElOptCopy = zrUtil.extend({}, newElOption);
  344. var existElOption = existList[index];
  345. var $action = newElOption.$action || 'merge';
  346. if ($action === 'merge') {
  347. if (existElOption) {
  348. // We can ensure that newElOptCopy and existElOption are not
  349. // the same object, so `merge` will not change newElOptCopy.
  350. zrUtil.merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize.
  351. layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, {
  352. ignoreSize: true
  353. }); // Will be used in render.
  354. layoutUtil.copyLayoutParams(newElOption, existElOption);
  355. } else {
  356. existList[index] = newElOptCopy;
  357. }
  358. } else if ($action === 'replace') {
  359. existList[index] = newElOptCopy;
  360. } else if ($action === 'remove') {
  361. // null will be cleaned later.
  362. existElOption && (existList[index] = null);
  363. }
  364. }
  365. function setLayoutInfoToExist(existItem, newElOption) {
  366. if (!existItem) {
  367. return;
  368. }
  369. existItem.hv = newElOption.hv = [// Rigid body, dont care `width`.
  370. isSetLoc(newElOption, ['left', 'right']), // Rigid body, dont care `height`.
  371. isSetLoc(newElOption, ['top', 'bottom'])]; // Give default group size. Otherwise layout error may occur.
  372. if (existItem.type === 'group') {
  373. existItem.width == null && (existItem.width = newElOption.width = 0);
  374. existItem.height == null && (existItem.height = newElOption.height = 0);
  375. }
  376. }