OptionManager.js 14 KB

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