OptionManager.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. var zrUtil = require("zrender/lib/core/util");
  2. var modelUtil = require("../util/model");
  3. var ComponentModel = require("./Component");
  4. /**
  5. * ECharts option manager
  6. *
  7. * @module {echarts/model/OptionManager}
  8. */
  9. var each = zrUtil.each;
  10. var clone = zrUtil.clone;
  11. var map = zrUtil.map;
  12. var merge = zrUtil.merge;
  13. var QUERY_REG = /^(min|max)?(.+)$/;
  14. /**
  15. * TERM EXPLANATIONS:
  16. *
  17. * [option]:
  18. *
  19. * An object that contains definitions of components. For example:
  20. * var option = {
  21. * title: {...},
  22. * legend: {...},
  23. * visualMap: {...},
  24. * series: [
  25. * {data: [...]},
  26. * {data: [...]},
  27. * ...
  28. * ]
  29. * };
  30. *
  31. * [rawOption]:
  32. *
  33. * An object input to echarts.setOption. 'rawOption' may be an
  34. * 'option', or may be an object contains multi-options. For example:
  35. * var option = {
  36. * baseOption: {
  37. * title: {...},
  38. * legend: {...},
  39. * series: [
  40. * {data: [...]},
  41. * {data: [...]},
  42. * ...
  43. * ]
  44. * },
  45. * timeline: {...},
  46. * options: [
  47. * {title: {...}, series: {data: [...]}},
  48. * {title: {...}, series: {data: [...]}},
  49. * ...
  50. * ],
  51. * media: [
  52. * {
  53. * query: {maxWidth: 320},
  54. * option: {series: {x: 20}, visualMap: {show: false}}
  55. * },
  56. * {
  57. * query: {minWidth: 320, maxWidth: 720},
  58. * option: {series: {x: 500}, visualMap: {show: true}}
  59. * },
  60. * {
  61. * option: {series: {x: 1200}, visualMap: {show: true}}
  62. * }
  63. * ]
  64. * };
  65. *
  66. * @alias module:echarts/model/OptionManager
  67. * @param {module:echarts/ExtensionAPI} api
  68. */
  69. function OptionManager(api) {
  70. /**
  71. * @private
  72. * @type {module:echarts/ExtensionAPI}
  73. */
  74. this._api = api;
  75. /**
  76. * @private
  77. * @type {Array.<number>}
  78. */
  79. this._timelineOptions = [];
  80. /**
  81. * @private
  82. * @type {Array.<Object>}
  83. */
  84. this._mediaList = [];
  85. /**
  86. * @private
  87. * @type {Object}
  88. */
  89. this._mediaDefault;
  90. /**
  91. * -1, means default.
  92. * empty means no media.
  93. * @private
  94. * @type {Array.<number>}
  95. */
  96. this._currentMediaIndices = [];
  97. /**
  98. * @private
  99. * @type {Object}
  100. */
  101. this._optionBackup;
  102. /**
  103. * @private
  104. * @type {Object}
  105. */
  106. this._newBaseOption;
  107. } // timeline.notMerge is not supported in ec3. Firstly there is rearly
  108. // case that notMerge is needed. Secondly supporting 'notMerge' requires
  109. // rawOption cloned and backuped when timeline changed, which does no
  110. // good to performance. What's more, that both timeline and setOption
  111. // method supply 'notMerge' brings complex and some problems.
  112. // Consider this case:
  113. // (step1) chart.setOption({timeline: {notMerge: false}, ...}, false);
  114. // (step2) chart.setOption({timeline: {notMerge: true}, ...}, false);
  115. OptionManager.prototype = {
  116. constructor: OptionManager,
  117. /**
  118. * @public
  119. * @param {Object} rawOption Raw option.
  120. * @param {module:echarts/model/Global} ecModel
  121. * @param {Array.<Function>} optionPreprocessorFuncs
  122. * @return {Object} Init option
  123. */
  124. setOption: function (rawOption, optionPreprocessorFuncs) {
  125. rawOption = clone(rawOption, true); // FIXME
  126. // 如果 timeline options 或者 media 中设置了某个属性,而baseOption中没有设置,则进行警告。
  127. var oldOptionBackup = this._optionBackup;
  128. var newParsedOption = parseRawOption.call(this, rawOption, optionPreprocessorFuncs, !oldOptionBackup);
  129. this._newBaseOption = newParsedOption.baseOption; // For setOption at second time (using merge mode);
  130. if (oldOptionBackup) {
  131. // Only baseOption can be merged.
  132. mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption); // For simplicity, timeline options and media options do not support merge,
  133. // that is, if you `setOption` twice and both has timeline options, the latter
  134. // timeline opitons will not be merged to the formers, but just substitude them.
  135. if (newParsedOption.timelineOptions.length) {
  136. oldOptionBackup.timelineOptions = newParsedOption.timelineOptions;
  137. }
  138. if (newParsedOption.mediaList.length) {
  139. oldOptionBackup.mediaList = newParsedOption.mediaList;
  140. }
  141. if (newParsedOption.mediaDefault) {
  142. oldOptionBackup.mediaDefault = newParsedOption.mediaDefault;
  143. }
  144. } else {
  145. this._optionBackup = newParsedOption;
  146. }
  147. },
  148. /**
  149. * @param {boolean} isRecreate
  150. * @return {Object}
  151. */
  152. mountOption: function (isRecreate) {
  153. var optionBackup = this._optionBackup; // TODO
  154. // 如果没有reset功能则不clone。
  155. this._timelineOptions = map(optionBackup.timelineOptions, clone);
  156. this._mediaList = map(optionBackup.mediaList, clone);
  157. this._mediaDefault = clone(optionBackup.mediaDefault);
  158. this._currentMediaIndices = [];
  159. return clone(isRecreate // this._optionBackup.baseOption, which is created at the first `setOption`
  160. // called, and is merged into every new option by inner method `mergeOption`
  161. // each time `setOption` called, can be only used in `isRecreate`, because
  162. // its reliability is under suspicion. In other cases option merge is
  163. // performed by `model.mergeOption`.
  164. ? optionBackup.baseOption : this._newBaseOption);
  165. },
  166. /**
  167. * @param {module:echarts/model/Global} ecModel
  168. * @return {Object}
  169. */
  170. getTimelineOption: function (ecModel) {
  171. var option;
  172. var timelineOptions = this._timelineOptions;
  173. if (timelineOptions.length) {
  174. // getTimelineOption can only be called after ecModel inited,
  175. // so we can get currentIndex from timelineModel.
  176. var timelineModel = ecModel.getComponent('timeline');
  177. if (timelineModel) {
  178. option = clone(timelineOptions[timelineModel.getCurrentIndex()], true);
  179. }
  180. }
  181. return option;
  182. },
  183. /**
  184. * @param {module:echarts/model/Global} ecModel
  185. * @return {Array.<Object>}
  186. */
  187. getMediaOption: function (ecModel) {
  188. var ecWidth = this._api.getWidth();
  189. var ecHeight = this._api.getHeight();
  190. var mediaList = this._mediaList;
  191. var mediaDefault = this._mediaDefault;
  192. var indices = [];
  193. var result = []; // No media defined.
  194. if (!mediaList.length && !mediaDefault) {
  195. return result;
  196. } // Multi media may be applied, the latter defined media has higher priority.
  197. for (var i = 0, len = mediaList.length; i < len; i++) {
  198. if (applyMediaQuery(mediaList[i].query, ecWidth, ecHeight)) {
  199. indices.push(i);
  200. }
  201. } // FIXME
  202. // 是否mediaDefault应该强制用户设置,否则可能修改不能回归。
  203. if (!indices.length && mediaDefault) {
  204. indices = [-1];
  205. }
  206. if (indices.length && !indicesEquals(indices, this._currentMediaIndices)) {
  207. result = map(indices, function (index) {
  208. return clone(index === -1 ? mediaDefault.option : mediaList[index].option);
  209. });
  210. } // Otherwise return nothing.
  211. this._currentMediaIndices = indices;
  212. return result;
  213. }
  214. };
  215. function parseRawOption(rawOption, optionPreprocessorFuncs, isNew) {
  216. var timelineOptions = [];
  217. var mediaList = [];
  218. var mediaDefault;
  219. var baseOption; // Compatible with ec2.
  220. var timelineOpt = rawOption.timeline;
  221. if (rawOption.baseOption) {
  222. baseOption = rawOption.baseOption;
  223. } // For timeline
  224. if (timelineOpt || rawOption.options) {
  225. baseOption = baseOption || {};
  226. timelineOptions = (rawOption.options || []).slice();
  227. } // For media query
  228. if (rawOption.media) {
  229. baseOption = baseOption || {};
  230. var media = rawOption.media;
  231. each(media, function (singleMedia) {
  232. if (singleMedia && singleMedia.option) {
  233. if (singleMedia.query) {
  234. mediaList.push(singleMedia);
  235. } else if (!mediaDefault) {
  236. // Use the first media default.
  237. mediaDefault = singleMedia;
  238. }
  239. }
  240. });
  241. } // For normal option
  242. if (!baseOption) {
  243. baseOption = rawOption;
  244. } // Set timelineOpt to baseOption in ec3,
  245. // which is convenient for merge option.
  246. if (!baseOption.timeline) {
  247. baseOption.timeline = timelineOpt;
  248. } // Preprocess.
  249. each([baseOption].concat(timelineOptions).concat(zrUtil.map(mediaList, function (media) {
  250. return media.option;
  251. })), function (option) {
  252. each(optionPreprocessorFuncs, function (preProcess) {
  253. preProcess(option, isNew);
  254. });
  255. });
  256. return {
  257. baseOption: baseOption,
  258. timelineOptions: timelineOptions,
  259. mediaDefault: mediaDefault,
  260. mediaList: mediaList
  261. };
  262. }
  263. /**
  264. * @see <http://www.w3.org/TR/css3-mediaqueries/#media1>
  265. * Support: width, height, aspectRatio
  266. * Can use max or min as prefix.
  267. */
  268. function applyMediaQuery(query, ecWidth, ecHeight) {
  269. var realMap = {
  270. width: ecWidth,
  271. height: ecHeight,
  272. aspectratio: ecWidth / ecHeight // lowser case for convenientce.
  273. };
  274. var applicatable = true;
  275. zrUtil.each(query, function (value, attr) {
  276. var matched = attr.match(QUERY_REG);
  277. if (!matched || !matched[1] || !matched[2]) {
  278. return;
  279. }
  280. var operator = matched[1];
  281. var realAttr = matched[2].toLowerCase();
  282. if (!compare(realMap[realAttr], value, operator)) {
  283. applicatable = false;
  284. }
  285. });
  286. return applicatable;
  287. }
  288. function compare(real, expect, operator) {
  289. if (operator === 'min') {
  290. return real >= expect;
  291. } else if (operator === 'max') {
  292. return real <= expect;
  293. } else {
  294. // Equals
  295. return real === expect;
  296. }
  297. }
  298. function indicesEquals(indices1, indices2) {
  299. // indices is always order by asc and has only finite number.
  300. return indices1.join(',') === indices2.join(',');
  301. }
  302. /**
  303. * Consider case:
  304. * `chart.setOption(opt1);`
  305. * Then user do some interaction like dataZoom, dataView changing.
  306. * `chart.setOption(opt2);`
  307. * Then user press 'reset button' in toolbox.
  308. *
  309. * After doing that all of the interaction effects should be reset, the
  310. * chart should be the same as the result of invoke
  311. * `chart.setOption(opt1); chart.setOption(opt2);`.
  312. *
  313. * Although it is not able ensure that
  314. * `chart.setOption(opt1); chart.setOption(opt2);` is equivalents to
  315. * `chart.setOption(merge(opt1, opt2));` exactly,
  316. * this might be the only simple way to implement that feature.
  317. *
  318. * MEMO: We've considered some other approaches:
  319. * 1. Each model handle its self restoration but not uniform treatment.
  320. * (Too complex in logic and error-prone)
  321. * 2. Use a shadow ecModel. (Performace expensive)
  322. */
  323. function mergeOption(oldOption, newOption) {
  324. newOption = newOption || {};
  325. each(newOption, function (newCptOpt, mainType) {
  326. if (newCptOpt == null) {
  327. return;
  328. }
  329. var oldCptOpt = oldOption[mainType];
  330. if (!ComponentModel.hasClass(mainType)) {
  331. oldOption[mainType] = merge(oldCptOpt, newCptOpt, true);
  332. } else {
  333. newCptOpt = modelUtil.normalizeToArray(newCptOpt);
  334. oldCptOpt = modelUtil.normalizeToArray(oldCptOpt);
  335. var mapResult = modelUtil.mappingToExists(oldCptOpt, newCptOpt);
  336. oldOption[mainType] = map(mapResult, function (item) {
  337. return item.option && item.exist ? merge(item.exist, item.option, true) : item.exist || item.option;
  338. });
  339. }
  340. });
  341. }
  342. var _default = OptionManager;
  343. module.exports = _default;