OptionManager.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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. * AUTO-GENERATED FILE. DO NOT MODIFY.
  21. */
  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. import { normalizeToArray // , MappingExistingItem, setComponentTypeToKeyInfo, mappingToExists
  41. } from '../util/model.js';
  42. import { each, clone, map, isTypedArray, setAsPrimitive, isArray, isObject // , HashMap , createHashMap, extend, merge,
  43. } from 'zrender/lib/core/util.js';
  44. import { error } from '../util/log.js';
  45. var QUERY_REG = /^(min|max)?(.+)$/; // Key: mainType
  46. // type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>;
  47. /**
  48. * TERM EXPLANATIONS:
  49. * See `ECOption` and `ECUnitOption` in `src/util/types.ts`.
  50. */
  51. var OptionManager =
  52. /** @class */
  53. function () {
  54. // timeline.notMerge is not supported in ec3. Firstly there is rearly
  55. // case that notMerge is needed. Secondly supporting 'notMerge' requires
  56. // rawOption cloned and backuped when timeline changed, which does no
  57. // good to performance. What's more, that both timeline and setOption
  58. // method supply 'notMerge' brings complex and some problems.
  59. // Consider this case:
  60. // (step1) chart.setOption({timeline: {notMerge: false}, ...}, false);
  61. // (step2) chart.setOption({timeline: {notMerge: true}, ...}, false);
  62. function OptionManager(api) {
  63. this._timelineOptions = [];
  64. this._mediaList = [];
  65. /**
  66. * -1, means default.
  67. * empty means no media.
  68. */
  69. this._currentMediaIndices = [];
  70. this._api = api;
  71. }
  72. OptionManager.prototype.setOption = function (rawOption, optionPreprocessorFuncs, opt) {
  73. if (rawOption) {
  74. // That set dat primitive is dangerous if user reuse the data when setOption again.
  75. each(normalizeToArray(rawOption.series), function (series) {
  76. series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data);
  77. });
  78. each(normalizeToArray(rawOption.dataset), function (dataset) {
  79. dataset && dataset.source && isTypedArray(dataset.source) && setAsPrimitive(dataset.source);
  80. });
  81. } // Caution: some series modify option data, if do not clone,
  82. // it should ensure that the repeat modify correctly
  83. // (create a new object when modify itself).
  84. rawOption = clone(rawOption); // FIXME
  85. // If some property is set in timeline options or media option but
  86. // not set in baseOption, a warning should be given.
  87. var optionBackup = this._optionBackup;
  88. var newParsedOption = parseRawOption(rawOption, optionPreprocessorFuncs, !optionBackup);
  89. this._newBaseOption = newParsedOption.baseOption; // For setOption at second time (using merge mode);
  90. if (optionBackup) {
  91. // FIXME
  92. // the restore merge solution is essentially incorrect.
  93. // the mapping can not be 100% consistent with ecModel, which probably brings
  94. // potential bug!
  95. // The first merge is delayed, because in most cases, users do not call `setOption` twice.
  96. // let fakeCmptsMap = this._fakeCmptsMap;
  97. // if (!fakeCmptsMap) {
  98. // fakeCmptsMap = this._fakeCmptsMap = createHashMap();
  99. // mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null);
  100. // }
  101. // mergeToBackupOption(
  102. // fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt
  103. // );
  104. // For simplicity, timeline options and media options do not support merge,
  105. // that is, if you `setOption` twice and both has timeline options, the latter
  106. // timeline options will not be merged to the former, but just substitute them.
  107. if (newParsedOption.timelineOptions.length) {
  108. optionBackup.timelineOptions = newParsedOption.timelineOptions;
  109. }
  110. if (newParsedOption.mediaList.length) {
  111. optionBackup.mediaList = newParsedOption.mediaList;
  112. }
  113. if (newParsedOption.mediaDefault) {
  114. optionBackup.mediaDefault = newParsedOption.mediaDefault;
  115. }
  116. } else {
  117. this._optionBackup = newParsedOption;
  118. }
  119. };
  120. OptionManager.prototype.mountOption = function (isRecreate) {
  121. var optionBackup = this._optionBackup;
  122. this._timelineOptions = optionBackup.timelineOptions;
  123. this._mediaList = optionBackup.mediaList;
  124. this._mediaDefault = optionBackup.mediaDefault;
  125. this._currentMediaIndices = [];
  126. return clone(isRecreate // this._optionBackup.baseOption, which is created at the first `setOption`
  127. // called, and is merged into every new option by inner method `mergeToBackupOption`
  128. // each time `setOption` called, can be only used in `isRecreate`, because
  129. // its reliability is under suspicion. In other cases option merge is
  130. // performed by `model.mergeOption`.
  131. ? optionBackup.baseOption : this._newBaseOption);
  132. };
  133. OptionManager.prototype.getTimelineOption = function (ecModel) {
  134. var option;
  135. var timelineOptions = this._timelineOptions;
  136. if (timelineOptions.length) {
  137. // getTimelineOption can only be called after ecModel inited,
  138. // so we can get currentIndex from timelineModel.
  139. var timelineModel = ecModel.getComponent('timeline');
  140. if (timelineModel) {
  141. option = clone( // FIXME:TS as TimelineModel or quivlant interface
  142. timelineOptions[timelineModel.getCurrentIndex()]);
  143. }
  144. }
  145. return option;
  146. };
  147. OptionManager.prototype.getMediaOption = function (ecModel) {
  148. var ecWidth = this._api.getWidth();
  149. var ecHeight = this._api.getHeight();
  150. var mediaList = this._mediaList;
  151. var mediaDefault = this._mediaDefault;
  152. var indices = [];
  153. var result = []; // No media defined.
  154. if (!mediaList.length && !mediaDefault) {
  155. return result;
  156. } // Multi media may be applied, the latter defined media has higher priority.
  157. for (var i = 0, len = mediaList.length; i < len; i++) {
  158. if (applyMediaQuery(mediaList[i].query, ecWidth, ecHeight)) {
  159. indices.push(i);
  160. }
  161. } // FIXME
  162. // Whether mediaDefault should force users to provide? Otherwise
  163. // the change by media query can not be recorvered.
  164. if (!indices.length && mediaDefault) {
  165. indices = [-1];
  166. }
  167. if (indices.length && !indicesEquals(indices, this._currentMediaIndices)) {
  168. result = map(indices, function (index) {
  169. return clone(index === -1 ? mediaDefault.option : mediaList[index].option);
  170. });
  171. } // Otherwise return nothing.
  172. this._currentMediaIndices = indices;
  173. return result;
  174. };
  175. return OptionManager;
  176. }();
  177. /**
  178. * [RAW_OPTION_PATTERNS]
  179. * (Note: "series: []" represents all other props in `ECUnitOption`)
  180. *
  181. * (1) No prop "baseOption" declared:
  182. * Root option is used as "baseOption" (except prop "options" and "media").
  183. * ```js
  184. * option = {
  185. * series: [],
  186. * timeline: {},
  187. * options: [],
  188. * };
  189. * option = {
  190. * series: [],
  191. * media: {},
  192. * };
  193. * option = {
  194. * series: [],
  195. * timeline: {},
  196. * options: [],
  197. * media: {},
  198. * }
  199. * ```
  200. *
  201. * (2) Prop "baseOption" declared:
  202. * If "baseOption" declared, `ECUnitOption` props can only be declared
  203. * inside "baseOption" except prop "timeline" (compat ec2).
  204. * ```js
  205. * option = {
  206. * baseOption: {
  207. * timeline: {},
  208. * series: [],
  209. * },
  210. * options: []
  211. * };
  212. * option = {
  213. * baseOption: {
  214. * series: [],
  215. * },
  216. * media: []
  217. * };
  218. * option = {
  219. * baseOption: {
  220. * timeline: {},
  221. * series: [],
  222. * },
  223. * options: []
  224. * media: []
  225. * };
  226. * option = {
  227. * // ec3 compat ec2: allow (only) `timeline` declared
  228. * // outside baseOption. Keep this setting for compat.
  229. * timeline: {},
  230. * baseOption: {
  231. * series: [],
  232. * },
  233. * options: [],
  234. * media: []
  235. * };
  236. * ```
  237. */
  238. function parseRawOption( // `rawOption` May be modified
  239. rawOption, optionPreprocessorFuncs, isNew) {
  240. var mediaList = [];
  241. var mediaDefault;
  242. var baseOption;
  243. var declaredBaseOption = rawOption.baseOption; // Compatible with ec2, [RAW_OPTION_PATTERNS] above.
  244. var timelineOnRoot = rawOption.timeline;
  245. var timelineOptionsOnRoot = rawOption.options;
  246. var mediaOnRoot = rawOption.media;
  247. var hasMedia = !!rawOption.media;
  248. var hasTimeline = !!(timelineOptionsOnRoot || timelineOnRoot || declaredBaseOption && declaredBaseOption.timeline);
  249. if (declaredBaseOption) {
  250. baseOption = declaredBaseOption; // For merge option.
  251. if (!baseOption.timeline) {
  252. baseOption.timeline = timelineOnRoot;
  253. }
  254. } // For convenience, enable to use the root option as the `baseOption`:
  255. // `{ ...normalOptionProps, media: [{ ... }, { ... }] }`
  256. else {
  257. if (hasTimeline || hasMedia) {
  258. rawOption.options = rawOption.media = null;
  259. }
  260. baseOption = rawOption;
  261. }
  262. if (hasMedia) {
  263. if (isArray(mediaOnRoot)) {
  264. each(mediaOnRoot, function (singleMedia) {
  265. if (process.env.NODE_ENV !== 'production') {
  266. // Real case of wrong config.
  267. if (singleMedia && !singleMedia.option && isObject(singleMedia.query) && isObject(singleMedia.query.option)) {
  268. error('Illegal media option. Must be like { media: [ { query: {}, option: {} } ] }');
  269. }
  270. }
  271. if (singleMedia && singleMedia.option) {
  272. if (singleMedia.query) {
  273. mediaList.push(singleMedia);
  274. } else if (!mediaDefault) {
  275. // Use the first media default.
  276. mediaDefault = singleMedia;
  277. }
  278. }
  279. });
  280. } else {
  281. if (process.env.NODE_ENV !== 'production') {
  282. // Real case of wrong config.
  283. error('Illegal media option. Must be an array. Like { media: [ {...}, {...} ] }');
  284. }
  285. }
  286. }
  287. doPreprocess(baseOption);
  288. each(timelineOptionsOnRoot, function (option) {
  289. return doPreprocess(option);
  290. });
  291. each(mediaList, function (media) {
  292. return doPreprocess(media.option);
  293. });
  294. function doPreprocess(option) {
  295. each(optionPreprocessorFuncs, function (preProcess) {
  296. preProcess(option, isNew);
  297. });
  298. }
  299. return {
  300. baseOption: baseOption,
  301. timelineOptions: timelineOptionsOnRoot || [],
  302. mediaDefault: mediaDefault,
  303. mediaList: mediaList
  304. };
  305. }
  306. /**
  307. * @see <http://www.w3.org/TR/css3-mediaqueries/#media1>
  308. * Support: width, height, aspectRatio
  309. * Can use max or min as prefix.
  310. */
  311. function applyMediaQuery(query, ecWidth, ecHeight) {
  312. var realMap = {
  313. width: ecWidth,
  314. height: ecHeight,
  315. aspectratio: ecWidth / ecHeight // lower case for convenience.
  316. };
  317. var applicable = true;
  318. each(query, function (value, attr) {
  319. var matched = attr.match(QUERY_REG);
  320. if (!matched || !matched[1] || !matched[2]) {
  321. return;
  322. }
  323. var operator = matched[1];
  324. var realAttr = matched[2].toLowerCase();
  325. if (!compare(realMap[realAttr], value, operator)) {
  326. applicable = false;
  327. }
  328. });
  329. return applicable;
  330. }
  331. function compare(real, expect, operator) {
  332. if (operator === 'min') {
  333. return real >= expect;
  334. } else if (operator === 'max') {
  335. return real <= expect;
  336. } else {
  337. // Equals
  338. return real === expect;
  339. }
  340. }
  341. function indicesEquals(indices1, indices2) {
  342. // indices is always order by asc and has only finite number.
  343. return indices1.join(',') === indices2.join(',');
  344. }
  345. /**
  346. * Consider case:
  347. * `chart.setOption(opt1);`
  348. * Then user do some interaction like dataZoom, dataView changing.
  349. * `chart.setOption(opt2);`
  350. * Then user press 'reset button' in toolbox.
  351. *
  352. * After doing that all of the interaction effects should be reset, the
  353. * chart should be the same as the result of invoke
  354. * `chart.setOption(opt1); chart.setOption(opt2);`.
  355. *
  356. * Although it is not able ensure that
  357. * `chart.setOption(opt1); chart.setOption(opt2);` is equivalents to
  358. * `chart.setOption(merge(opt1, opt2));` exactly,
  359. * this might be the only simple way to implement that feature.
  360. *
  361. * MEMO: We've considered some other approaches:
  362. * 1. Each model handles its self restoration but not uniform treatment.
  363. * (Too complex in logic and error-prone)
  364. * 2. Use a shadow ecModel. (Performance expensive)
  365. *
  366. * FIXME: A possible solution:
  367. * Add a extra level of model for each component model. The inheritance chain would be:
  368. * ecModel <- componentModel <- componentActionModel <- dataItemModel
  369. * And all of the actions can only modify the `componentActionModel` rather than
  370. * `componentModel`. `setOption` will only modify the `ecModel` and `componentModel`.
  371. * When "resotre" action triggered, model from `componentActionModel` will be discarded
  372. * instead of recreating the "ecModel" from the "_optionBackup".
  373. */
  374. // function mergeToBackupOption(
  375. // fakeCmptsMap: FakeComponentsMap,
  376. // // `tarOption` Can be null/undefined, means init
  377. // tarOption: ECUnitOption,
  378. // newOption: ECUnitOption,
  379. // // Can be null/undefined
  380. // opt: InnerSetOptionOpts
  381. // ): void {
  382. // newOption = newOption || {} as ECUnitOption;
  383. // const notInit = !!tarOption;
  384. // each(newOption, function (newOptsInMainType, mainType) {
  385. // if (newOptsInMainType == null) {
  386. // return;
  387. // }
  388. // if (!ComponentModel.hasClass(mainType)) {
  389. // if (tarOption) {
  390. // tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true);
  391. // }
  392. // }
  393. // else {
  394. // const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null;
  395. // const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || [];
  396. // const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null;
  397. // const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []);
  398. // const mappingResult = mappingToExists(
  399. // oldFakeCmptsInMainType,
  400. // normalizeToArray(newOptsInMainType),
  401. // (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge'
  402. // );
  403. // setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor);
  404. // each(mappingResult, function (resultItem, index) {
  405. // // The same logic as `Global.ts#_mergeOption`.
  406. // let fakeCmpt = resultItem.existing;
  407. // const newOption = resultItem.newOption;
  408. // const keyInfo = resultItem.keyInfo;
  409. // let fakeCmptOpt;
  410. // if (!newOption) {
  411. // fakeCmptOpt = oldTarOptsInMainType[index];
  412. // }
  413. // else {
  414. // if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) {
  415. // fakeCmpt.name = keyInfo.name;
  416. // if (notInit) {
  417. // fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true);
  418. // }
  419. // }
  420. // else {
  421. // fakeCmpt = extend({}, keyInfo);
  422. // if (notInit) {
  423. // fakeCmptOpt = clone(newOption);
  424. // }
  425. // }
  426. // }
  427. // if (fakeCmpt) {
  428. // notInit && resultTarOptsInMainType.push(fakeCmptOpt);
  429. // resultFakeCmptsInMainType.push(fakeCmpt);
  430. // }
  431. // else {
  432. // notInit && resultTarOptsInMainType.push(void 0);
  433. // resultFakeCmptsInMainType.push(void 0);
  434. // }
  435. // });
  436. // }
  437. // });
  438. // }
  439. export default OptionManager;