OptionManager.js 14 KB

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