OptionManager.js 13 KB

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