OptionManager.js 17 KB

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