VisualMapping.js 16 KB

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