VisualMapping.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  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 zrColor = require("zrender/lib/tool/color");
  21. var _number = require("../util/number");
  22. var linearMap = _number.linearMap;
  23. /*
  24. * Licensed to the Apache Software Foundation (ASF) under one
  25. * or more contributor license agreements. See the NOTICE file
  26. * distributed with this work for additional information
  27. * regarding copyright ownership. The ASF licenses this file
  28. * to you under the Apache License, Version 2.0 (the
  29. * "License"); you may not use this file except in compliance
  30. * with the License. You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing,
  35. * software distributed under the License is distributed on an
  36. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  37. * KIND, either express or implied. See the License for the
  38. * specific language governing permissions and limitations
  39. * under the License.
  40. */
  41. var each = zrUtil.each;
  42. var isObject = zrUtil.isObject;
  43. var CATEGORY_DEFAULT_VISUAL_INDEX = -1;
  44. /**
  45. * @param {Object} option
  46. * @param {string} [option.type] See visualHandlers.
  47. * @param {string} [option.mappingMethod] 'linear' or 'piecewise' or 'category' or 'fixed'
  48. * @param {Array.<number>=} [option.dataExtent] [minExtent, maxExtent],
  49. * required when mappingMethod is 'linear'
  50. * @param {Array.<Object>=} [option.pieceList] [
  51. * {value: someValue},
  52. * {interval: [min1, max1], visual: {...}},
  53. * {interval: [min2, max2]}
  54. * ],
  55. * required when mappingMethod is 'piecewise'.
  56. * Visual for only each piece can be specified.
  57. * @param {Array.<string|Object>=} [option.categories] ['cate1', 'cate2']
  58. * required when mappingMethod is 'category'.
  59. * If no option.categories, categories is set
  60. * as [0, 1, 2, ...].
  61. * @param {boolean} [option.loop=false] Whether loop mapping when mappingMethod is 'category'.
  62. * @param {(Array|Object|*)} [option.visual] Visual data.
  63. * when mappingMethod is 'category',
  64. * visual data can be array or object
  65. * (like: {cate1: '#222', none: '#fff'})
  66. * or primary types (which represents
  67. * default category visual), otherwise visual
  68. * can be array or primary (which will be
  69. * normalized to array).
  70. *
  71. */
  72. var VisualMapping = function (option) {
  73. var mappingMethod = option.mappingMethod;
  74. var visualType = option.type;
  75. /**
  76. * @readOnly
  77. * @type {Object}
  78. */
  79. var thisOption = this.option = zrUtil.clone(option);
  80. /**
  81. * @readOnly
  82. * @type {string}
  83. */
  84. this.type = visualType;
  85. /**
  86. * @readOnly
  87. * @type {string}
  88. */
  89. this.mappingMethod = mappingMethod;
  90. /**
  91. * @private
  92. * @type {Function}
  93. */
  94. this._normalizeData = normalizers[mappingMethod];
  95. var visualHandler = visualHandlers[visualType];
  96. /**
  97. * @public
  98. * @type {Function}
  99. */
  100. this.applyVisual = visualHandler.applyVisual;
  101. /**
  102. * @public
  103. * @type {Function}
  104. */
  105. this.getColorMapper = visualHandler.getColorMapper;
  106. /**
  107. * @private
  108. * @type {Function}
  109. */
  110. this._doMap = visualHandler._doMap[mappingMethod];
  111. if (mappingMethod === 'piecewise') {
  112. normalizeVisualRange(thisOption);
  113. preprocessForPiecewise(thisOption);
  114. } else if (mappingMethod === 'category') {
  115. thisOption.categories ? preprocessForSpecifiedCategory(thisOption) // categories is ordinal when thisOption.categories not specified,
  116. // which need no more preprocess except normalize visual.
  117. : normalizeVisualRange(thisOption, true);
  118. } else {
  119. // mappingMethod === 'linear' or 'fixed'
  120. zrUtil.assert(mappingMethod !== 'linear' || thisOption.dataExtent);
  121. normalizeVisualRange(thisOption);
  122. }
  123. };
  124. VisualMapping.prototype = {
  125. constructor: VisualMapping,
  126. mapValueToVisual: function (value) {
  127. var normalized = this._normalizeData(value);
  128. return this._doMap(normalized, value);
  129. },
  130. getNormalizer: function () {
  131. return zrUtil.bind(this._normalizeData, this);
  132. }
  133. };
  134. var visualHandlers = VisualMapping.visualHandlers = {
  135. color: {
  136. applyVisual: makeApplyVisual('color'),
  137. /**
  138. * Create a mapper function
  139. * @return {Function}
  140. */
  141. getColorMapper: function () {
  142. var thisOption = this.option;
  143. return zrUtil.bind(thisOption.mappingMethod === 'category' ? function (value, isNormalized) {
  144. !isNormalized && (value = this._normalizeData(value));
  145. return doMapCategory.call(this, value);
  146. } : function (value, isNormalized, out) {
  147. // If output rgb array
  148. // which will be much faster and useful in pixel manipulation
  149. var returnRGBArray = !!out;
  150. !isNormalized && (value = this._normalizeData(value));
  151. out = zrColor.fastLerp(value, thisOption.parsedVisual, out);
  152. return returnRGBArray ? out : zrColor.stringify(out, 'rgba');
  153. }, this);
  154. },
  155. _doMap: {
  156. linear: function (normalized) {
  157. return zrColor.stringify(zrColor.fastLerp(normalized, this.option.parsedVisual), 'rgba');
  158. },
  159. category: doMapCategory,
  160. piecewise: function (normalized, value) {
  161. var result = getSpecifiedVisual.call(this, value);
  162. if (result == null) {
  163. result = zrColor.stringify(zrColor.fastLerp(normalized, this.option.parsedVisual), 'rgba');
  164. }
  165. return result;
  166. },
  167. fixed: doMapFixed
  168. }
  169. },
  170. colorHue: makePartialColorVisualHandler(function (color, value) {
  171. return zrColor.modifyHSL(color, value);
  172. }),
  173. colorSaturation: makePartialColorVisualHandler(function (color, value) {
  174. return zrColor.modifyHSL(color, null, value);
  175. }),
  176. colorLightness: makePartialColorVisualHandler(function (color, value) {
  177. return zrColor.modifyHSL(color, null, null, value);
  178. }),
  179. colorAlpha: makePartialColorVisualHandler(function (color, value) {
  180. return zrColor.modifyAlpha(color, value);
  181. }),
  182. opacity: {
  183. applyVisual: makeApplyVisual('opacity'),
  184. _doMap: makeDoMap([0, 1])
  185. },
  186. liftZ: {
  187. applyVisual: makeApplyVisual('liftZ'),
  188. _doMap: {
  189. linear: doMapFixed,
  190. category: doMapFixed,
  191. piecewise: doMapFixed,
  192. fixed: doMapFixed
  193. }
  194. },
  195. symbol: {
  196. applyVisual: function (value, getter, setter) {
  197. var symbolCfg = this.mapValueToVisual(value);
  198. if (zrUtil.isString(symbolCfg)) {
  199. setter('symbol', symbolCfg);
  200. } else if (isObject(symbolCfg)) {
  201. for (var name in symbolCfg) {
  202. if (symbolCfg.hasOwnProperty(name)) {
  203. setter(name, symbolCfg[name]);
  204. }
  205. }
  206. }
  207. },
  208. _doMap: {
  209. linear: doMapToArray,
  210. category: doMapCategory,
  211. piecewise: function (normalized, value) {
  212. var result = getSpecifiedVisual.call(this, value);
  213. if (result == null) {
  214. result = doMapToArray.call(this, normalized);
  215. }
  216. return result;
  217. },
  218. fixed: doMapFixed
  219. }
  220. },
  221. symbolSize: {
  222. applyVisual: makeApplyVisual('symbolSize'),
  223. _doMap: makeDoMap([0, 1])
  224. }
  225. };
  226. function preprocessForPiecewise(thisOption) {
  227. var pieceList = thisOption.pieceList;
  228. thisOption.hasSpecialVisual = false;
  229. zrUtil.each(pieceList, function (piece, index) {
  230. piece.originIndex = index; // piece.visual is "result visual value" but not
  231. // a visual range, so it does not need to be normalized.
  232. if (piece.visual != null) {
  233. thisOption.hasSpecialVisual = true;
  234. }
  235. });
  236. }
  237. function preprocessForSpecifiedCategory(thisOption) {
  238. // Hash categories.
  239. var categories = thisOption.categories;
  240. var visual = thisOption.visual;
  241. var categoryMap = thisOption.categoryMap = {};
  242. each(categories, function (cate, index) {
  243. categoryMap[cate] = index;
  244. }); // Process visual map input.
  245. if (!zrUtil.isArray(visual)) {
  246. var visualArr = [];
  247. if (zrUtil.isObject(visual)) {
  248. each(visual, function (v, cate) {
  249. var index = categoryMap[cate];
  250. visualArr[index != null ? index : CATEGORY_DEFAULT_VISUAL_INDEX] = v;
  251. });
  252. } else {
  253. // Is primary type, represents default visual.
  254. visualArr[CATEGORY_DEFAULT_VISUAL_INDEX] = visual;
  255. }
  256. visual = setVisualToOption(thisOption, visualArr);
  257. } // Remove categories that has no visual,
  258. // then we can mapping them to CATEGORY_DEFAULT_VISUAL_INDEX.
  259. for (var i = categories.length - 1; i >= 0; i--) {
  260. if (visual[i] == null) {
  261. delete categoryMap[categories[i]];
  262. categories.pop();
  263. }
  264. }
  265. }
  266. function normalizeVisualRange(thisOption, isCategory) {
  267. var visual = thisOption.visual;
  268. var visualArr = [];
  269. if (zrUtil.isObject(visual)) {
  270. each(visual, function (v) {
  271. visualArr.push(v);
  272. });
  273. } else if (visual != null) {
  274. visualArr.push(visual);
  275. }
  276. var doNotNeedPair = {
  277. color: 1,
  278. symbol: 1
  279. };
  280. if (!isCategory && visualArr.length === 1 && !doNotNeedPair.hasOwnProperty(thisOption.type)) {
  281. // Do not care visualArr.length === 0, which is illegal.
  282. visualArr[1] = visualArr[0];
  283. }
  284. setVisualToOption(thisOption, visualArr);
  285. }
  286. function makePartialColorVisualHandler(applyValue) {
  287. return {
  288. applyVisual: function (value, getter, setter) {
  289. value = this.mapValueToVisual(value); // Must not be array value
  290. setter('color', applyValue(getter('color'), value));
  291. },
  292. _doMap: makeDoMap([0, 1])
  293. };
  294. }
  295. function doMapToArray(normalized) {
  296. var visual = this.option.visual;
  297. return visual[Math.round(linearMap(normalized, [0, 1], [0, visual.length - 1], true))] || {};
  298. }
  299. function makeApplyVisual(visualType) {
  300. return function (value, getter, setter) {
  301. setter(visualType, this.mapValueToVisual(value));
  302. };
  303. }
  304. function doMapCategory(normalized) {
  305. var visual = this.option.visual;
  306. return visual[this.option.loop && normalized !== CATEGORY_DEFAULT_VISUAL_INDEX ? normalized % visual.length : normalized];
  307. }
  308. function doMapFixed() {
  309. return this.option.visual[0];
  310. }
  311. function makeDoMap(sourceExtent) {
  312. return {
  313. linear: function (normalized) {
  314. return linearMap(normalized, sourceExtent, this.option.visual, true);
  315. },
  316. category: doMapCategory,
  317. piecewise: function (normalized, value) {
  318. var result = getSpecifiedVisual.call(this, value);
  319. if (result == null) {
  320. result = linearMap(normalized, sourceExtent, this.option.visual, true);
  321. }
  322. return result;
  323. },
  324. fixed: doMapFixed
  325. };
  326. }
  327. function getSpecifiedVisual(value) {
  328. var thisOption = this.option;
  329. var pieceList = thisOption.pieceList;
  330. if (thisOption.hasSpecialVisual) {
  331. var pieceIndex = VisualMapping.findPieceIndex(value, pieceList);
  332. var piece = pieceList[pieceIndex];
  333. if (piece && piece.visual) {
  334. return piece.visual[this.type];
  335. }
  336. }
  337. }
  338. function setVisualToOption(thisOption, visualArr) {
  339. thisOption.visual = visualArr;
  340. if (thisOption.type === 'color') {
  341. thisOption.parsedVisual = zrUtil.map(visualArr, function (item) {
  342. return zrColor.parse(item);
  343. });
  344. }
  345. return visualArr;
  346. }
  347. /**
  348. * Normalizers by mapping methods.
  349. */
  350. var normalizers = {
  351. linear: function (value) {
  352. return linearMap(value, this.option.dataExtent, [0, 1], true);
  353. },
  354. piecewise: function (value) {
  355. var pieceList = this.option.pieceList;
  356. var pieceIndex = VisualMapping.findPieceIndex(value, pieceList, true);
  357. if (pieceIndex != null) {
  358. return linearMap(pieceIndex, [0, pieceList.length - 1], [0, 1], true);
  359. }
  360. },
  361. category: function (value) {
  362. var index = this.option.categories ? this.option.categoryMap[value] : value; // ordinal
  363. return index == null ? CATEGORY_DEFAULT_VISUAL_INDEX : index;
  364. },
  365. fixed: zrUtil.noop
  366. };
  367. /**
  368. * List available visual types.
  369. *
  370. * @public
  371. * @return {Array.<string>}
  372. */
  373. VisualMapping.listVisualTypes = function () {
  374. var visualTypes = [];
  375. zrUtil.each(visualHandlers, function (handler, key) {
  376. visualTypes.push(key);
  377. });
  378. return visualTypes;
  379. };
  380. /**
  381. * @public
  382. */
  383. VisualMapping.addVisualHandler = function (name, handler) {
  384. visualHandlers[name] = handler;
  385. };
  386. /**
  387. * @public
  388. */
  389. VisualMapping.isValidType = function (visualType) {
  390. return visualHandlers.hasOwnProperty(visualType);
  391. };
  392. /**
  393. * Convinent method.
  394. * Visual can be Object or Array or primary type.
  395. *
  396. * @public
  397. */
  398. VisualMapping.eachVisual = function (visual, callback, context) {
  399. if (zrUtil.isObject(visual)) {
  400. zrUtil.each(visual, callback, context);
  401. } else {
  402. callback.call(context, visual);
  403. }
  404. };
  405. VisualMapping.mapVisual = function (visual, callback, context) {
  406. var isPrimary;
  407. var newVisual = zrUtil.isArray(visual) ? [] : zrUtil.isObject(visual) ? {} : (isPrimary = true, null);
  408. VisualMapping.eachVisual(visual, function (v, key) {
  409. var newVal = callback.call(context, v, key);
  410. isPrimary ? newVisual = newVal : newVisual[key] = newVal;
  411. });
  412. return newVisual;
  413. };
  414. /**
  415. * @public
  416. * @param {Object} obj
  417. * @return {Object} new object containers visual values.
  418. * If no visuals, return null.
  419. */
  420. VisualMapping.retrieveVisuals = function (obj) {
  421. var ret = {};
  422. var hasVisual;
  423. obj && each(visualHandlers, function (h, visualType) {
  424. if (obj.hasOwnProperty(visualType)) {
  425. ret[visualType] = obj[visualType];
  426. hasVisual = true;
  427. }
  428. });
  429. return hasVisual ? ret : null;
  430. };
  431. /**
  432. * Give order to visual types, considering colorSaturation, colorAlpha depends on color.
  433. *
  434. * @public
  435. * @param {(Object|Array)} visualTypes If Object, like: {color: ..., colorSaturation: ...}
  436. * IF Array, like: ['color', 'symbol', 'colorSaturation']
  437. * @return {Array.<string>} Sorted visual types.
  438. */
  439. VisualMapping.prepareVisualTypes = function (visualTypes) {
  440. if (isObject(visualTypes)) {
  441. var types = [];
  442. each(visualTypes, function (item, type) {
  443. types.push(type);
  444. });
  445. visualTypes = types;
  446. } else if (zrUtil.isArray(visualTypes)) {
  447. visualTypes = visualTypes.slice();
  448. } else {
  449. return [];
  450. }
  451. visualTypes.sort(function (type1, type2) {
  452. // color should be front of colorSaturation, colorAlpha, ...
  453. // symbol and symbolSize do not matter.
  454. return type2 === 'color' && type1 !== 'color' && type1.indexOf('color') === 0 ? 1 : -1;
  455. });
  456. return visualTypes;
  457. };
  458. /**
  459. * 'color', 'colorSaturation', 'colorAlpha', ... are depends on 'color'.
  460. * Other visuals are only depends on themself.
  461. *
  462. * @public
  463. * @param {string} visualType1
  464. * @param {string} visualType2
  465. * @return {boolean}
  466. */
  467. VisualMapping.dependsOn = function (visualType1, visualType2) {
  468. return visualType2 === 'color' ? !!(visualType1 && visualType1.indexOf(visualType2) === 0) : visualType1 === visualType2;
  469. };
  470. /**
  471. * @param {number} value
  472. * @param {Array.<Object>} pieceList [{value: ..., interval: [min, max]}, ...]
  473. * Always from small to big.
  474. * @param {boolean} [findClosestWhenOutside=false]
  475. * @return {number} index
  476. */
  477. VisualMapping.findPieceIndex = function (value, pieceList, findClosestWhenOutside) {
  478. var possibleI;
  479. var abs = Infinity; // value has the higher priority.
  480. for (var i = 0, len = pieceList.length; i < len; i++) {
  481. var pieceValue = pieceList[i].value;
  482. if (pieceValue != null) {
  483. if (pieceValue === value // FIXME
  484. // It is supposed to compare value according to value type of dimension,
  485. // but currently value type can exactly be string or number.
  486. // Compromise for numeric-like string (like '12'), especially
  487. // in the case that visualMap.categories is ['22', '33'].
  488. || typeof pieceValue === 'string' && pieceValue === value + '') {
  489. return i;
  490. }
  491. findClosestWhenOutside && updatePossible(pieceValue, i);
  492. }
  493. }
  494. for (var i = 0, len = pieceList.length; i < len; i++) {
  495. var piece = pieceList[i];
  496. var interval = piece.interval;
  497. var close = piece.close;
  498. if (interval) {
  499. if (interval[0] === -Infinity) {
  500. if (littleThan(close[1], value, interval[1])) {
  501. return i;
  502. }
  503. } else if (interval[1] === Infinity) {
  504. if (littleThan(close[0], interval[0], value)) {
  505. return i;
  506. }
  507. } else if (littleThan(close[0], interval[0], value) && littleThan(close[1], value, interval[1])) {
  508. return i;
  509. }
  510. findClosestWhenOutside && updatePossible(interval[0], i);
  511. findClosestWhenOutside && updatePossible(interval[1], i);
  512. }
  513. }
  514. if (findClosestWhenOutside) {
  515. return value === Infinity ? pieceList.length - 1 : value === -Infinity ? 0 : possibleI;
  516. }
  517. function updatePossible(val, index) {
  518. var newAbs = Math.abs(val - value);
  519. if (newAbs < abs) {
  520. abs = newAbs;
  521. possibleI = index;
  522. }
  523. }
  524. };
  525. function littleThan(close, a, b) {
  526. return close ? a <= b : a < b;
  527. }
  528. var _default = VisualMapping;
  529. module.exports = _default;