(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.echarts = {}))); }(this, (function (exports) { 'use strict'; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // (1) The code `if (__DEV__) ...` can be removed by build tool. // (2) If intend to use `__DEV__`, this module should be imported. Use a global // variable `__DEV__` may cause that miss the declaration (see #6535), or the // declaration is behind of the using position (for example in `Model.extent`, // And tools like rollup can not analysis the dependency if not import). var dev; // In browser if (typeof window !== 'undefined') { dev = window.__DEV__; } // In node else if (typeof global !== 'undefined') { dev = global.__DEV__; } if (typeof dev === 'undefined') { dev = true; } var __DEV__ = dev; /** * zrender: 生成唯一id * * @author errorrik (errorrik@gmail.com) */ var idStart = 0x0907; var guid = function () { return idStart++; }; /** * echarts设备环境识别 * * @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 * @author firede[firede@firede.us] * @desc thanks zepto. */ /* global wx */ var env = {}; if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') { // In Weixin Application env = { browser: {}, os: {}, node: false, wxa: true, // Weixin Application canvasSupported: true, svgSupported: false, touchEventsSupported: true, domSupported: false }; } else if (typeof document === 'undefined' && typeof self !== 'undefined') { // In worker env = { browser: {}, os: {}, node: false, worker: true, canvasSupported: true, domSupported: false }; } else if (typeof navigator === 'undefined') { // In node env = { browser: {}, os: {}, node: true, worker: false, // Assume canvas is supported canvasSupported: true, svgSupported: true, domSupported: false }; } else { env = detect(navigator.userAgent); } var env$1 = env; // Zepto.js // (c) 2010-2013 Thomas Fuchs // Zepto.js may be freely distributed under the MIT license. function detect(ua) { var os = {}; var browser = {}; // var webkit = ua.match(/Web[kK]it[\/]{0,1}([\d.]+)/); // var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/); // var ipad = ua.match(/(iPad).*OS\s([\d_]+)/); // var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/); // var iphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/); // var webos = ua.match(/(webOS|hpwOS)[\s\/]([\d.]+)/); // var touchpad = webos && ua.match(/TouchPad/); // var kindle = ua.match(/Kindle\/([\d.]+)/); // var silk = ua.match(/Silk\/([\d._]+)/); // var blackberry = ua.match(/(BlackBerry).*Version\/([\d.]+)/); // var bb10 = ua.match(/(BB10).*Version\/([\d.]+)/); // var rimtabletos = ua.match(/(RIM\sTablet\sOS)\s([\d.]+)/); // var playbook = ua.match(/PlayBook/); // var chrome = ua.match(/Chrome\/([\d.]+)/) || ua.match(/CriOS\/([\d.]+)/); var firefox = ua.match(/Firefox\/([\d.]+)/); // var safari = webkit && ua.match(/Mobile\//) && !chrome; // var webview = ua.match(/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/) && !chrome; var ie = ua.match(/MSIE\s([\d.]+)/) // IE 11 Trident/7.0; rv:11.0 || ua.match(/Trident\/.+?rv:(([\d.]+))/); var edge = ua.match(/Edge\/([\d.]+)/); // IE 12 and 12+ var weChat = (/micromessenger/i).test(ua); // Todo: clean this up with a better OS/browser seperation: // - discern (more) between multiple browsers on android // - decide if kindle fire in silk mode is android or not // - Firefox on Android doesn't specify the Android version // - possibly devide in os, device and browser hashes // if (browser.webkit = !!webkit) browser.version = webkit[1]; // if (android) os.android = true, os.version = android[2]; // if (iphone && !ipod) os.ios = os.iphone = true, os.version = iphone[2].replace(/_/g, '.'); // if (ipad) os.ios = os.ipad = true, os.version = ipad[2].replace(/_/g, '.'); // if (ipod) os.ios = os.ipod = true, os.version = ipod[3] ? ipod[3].replace(/_/g, '.') : null; // if (webos) os.webos = true, os.version = webos[2]; // if (touchpad) os.touchpad = true; // if (blackberry) os.blackberry = true, os.version = blackberry[2]; // if (bb10) os.bb10 = true, os.version = bb10[2]; // if (rimtabletos) os.rimtabletos = true, os.version = rimtabletos[2]; // if (playbook) browser.playbook = true; // if (kindle) os.kindle = true, os.version = kindle[1]; // if (silk) browser.silk = true, browser.version = silk[1]; // if (!silk && os.android && ua.match(/Kindle Fire/)) browser.silk = true; // if (chrome) browser.chrome = true, browser.version = chrome[1]; if (firefox) { browser.firefox = true; browser.version = firefox[1]; } // if (safari && (ua.match(/Safari/) || !!os.ios)) browser.safari = true; // if (webview) browser.webview = true; if (ie) { browser.ie = true; browser.version = ie[1]; } if (edge) { browser.edge = true; browser.version = edge[1]; } // It is difficult to detect WeChat in Win Phone precisely, because ua can // not be set on win phone. So we do not consider Win Phone. if (weChat) { browser.weChat = true; } // os.tablet = !!(ipad || playbook || (android && !ua.match(/Mobile/)) || // (firefox && ua.match(/Tablet/)) || (ie && !ua.match(/Phone/) && ua.match(/Touch/))); // os.phone = !!(!os.tablet && !os.ipod && (android || iphone || webos || // (chrome && ua.match(/Android/)) || (chrome && ua.match(/CriOS\/([\d.]+)/)) || // (firefox && ua.match(/Mobile/)) || (ie && ua.match(/Touch/)))); return { browser: browser, os: os, node: false, // 原生canvas支持,改极端点了 // canvasSupported : !(browser.ie && parseFloat(browser.version) < 9) canvasSupported: !!document.createElement('canvas').getContext, svgSupported: typeof SVGRect !== 'undefined', // works on most browsers // IE10/11 does not support touch event, and MS Edge supports them but not by // default, so we dont check navigator.maxTouchPoints for them here. touchEventsSupported: 'ontouchstart' in window && !browser.ie && !browser.edge, // . pointerEventsSupported: // (1) Firefox supports pointer but not by default, only MS browsers are reliable on pointer // events currently. So we dont use that on other browsers unless tested sufficiently. // For example, in iOS 13 Mobile Chromium 78, if the touching behavior starts page // scroll, the `pointermove` event can not be fired any more. That will break some // features like "pan horizontally to move something and pan vertically to page scroll". // The horizontal pan probably be interrupted by the casually triggered page scroll. // (2) Although IE 10 supports pointer event, it use old style and is different from the // standard. So we exclude that. (IE 10 is hardly used on touch device) 'onpointerdown' in window && (browser.edge || (browser.ie && browser.version >= 11)), // passiveSupported: detectPassiveSupport() domSupported: typeof document !== 'undefined' }; } // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection // function detectPassiveSupport() { // // Test via a getter in the options object to see if the passive property is accessed // var supportsPassive = false; // try { // var opts = Object.defineProperty({}, 'passive', { // get: function() { // supportsPassive = true; // } // }); // window.addEventListener('testPassive', function() {}, opts); // } catch (e) { // } // return supportsPassive; // } /** * @module zrender/core/util */ // 用于处理merge时无法遍历Date等对象的问题 var BUILTIN_OBJECT = { '[object Function]': 1, '[object RegExp]': 1, '[object Date]': 1, '[object Error]': 1, '[object CanvasGradient]': 1, '[object CanvasPattern]': 1, // For node-canvas '[object Image]': 1, '[object Canvas]': 1 }; var TYPED_ARRAY = { '[object Int8Array]': 1, '[object Uint8Array]': 1, '[object Uint8ClampedArray]': 1, '[object Int16Array]': 1, '[object Uint16Array]': 1, '[object Int32Array]': 1, '[object Uint32Array]': 1, '[object Float32Array]': 1, '[object Float64Array]': 1 }; var objToString = Object.prototype.toString; var arrayProto = Array.prototype; var nativeForEach = arrayProto.forEach; var nativeFilter = arrayProto.filter; var nativeSlice = arrayProto.slice; var nativeMap = arrayProto.map; var nativeReduce = arrayProto.reduce; // Avoid assign to an exported variable, for transforming to cjs. var methods = {}; function $override(name, fn) { // Clear ctx instance for different environment if (name === 'createCanvas') { _ctx = null; } methods[name] = fn; } /** * Those data types can be cloned: * Plain object, Array, TypedArray, number, string, null, undefined. * Those data types will be assgined using the orginal data: * BUILTIN_OBJECT * Instance of user defined class will be cloned to a plain object, without * properties in prototype. * Other data types is not supported (not sure what will happen). * * Caution: do not support clone Date, for performance consideration. * (There might be a large number of date in `series.data`). * So date should not be modified in and out of echarts. * * @param {*} source * @return {*} new */ function clone(source) { if (source == null || typeof source !== 'object') { return source; } var result = source; var typeStr = objToString.call(source); if (typeStr === '[object Array]') { if (!isPrimitive(source)) { result = []; for (var i = 0, len = source.length; i < len; i++) { result[i] = clone(source[i]); } } } else if (TYPED_ARRAY[typeStr]) { if (!isPrimitive(source)) { var Ctor = source.constructor; if (source.constructor.from) { result = Ctor.from(source); } else { result = new Ctor(source.length); for (var i = 0, len = source.length; i < len; i++) { result[i] = clone(source[i]); } } } } else if (!BUILTIN_OBJECT[typeStr] && !isPrimitive(source) && !isDom(source)) { result = {}; for (var key in source) { if (source.hasOwnProperty(key)) { result[key] = clone(source[key]); } } } return result; } /** * @memberOf module:zrender/core/util * @param {*} target * @param {*} source * @param {boolean} [overwrite=false] */ function merge(target, source, overwrite) { // We should escapse that source is string // and enter for ... in ... if (!isObject$1(source) || !isObject$1(target)) { return overwrite ? clone(source) : target; } for (var key in source) { if (source.hasOwnProperty(key)) { var targetProp = target[key]; var sourceProp = source[key]; if (isObject$1(sourceProp) && isObject$1(targetProp) && !isArray(sourceProp) && !isArray(targetProp) && !isDom(sourceProp) && !isDom(targetProp) && !isBuiltInObject(sourceProp) && !isBuiltInObject(targetProp) && !isPrimitive(sourceProp) && !isPrimitive(targetProp) ) { // 如果需要递归覆盖,就递归调用merge merge(targetProp, sourceProp, overwrite); } else if (overwrite || !(key in target)) { // 否则只处理overwrite为true,或者在目标对象中没有此属性的情况 // NOTE,在 target[key] 不存在的时候也是直接覆盖 target[key] = clone(source[key], true); } } } return target; } /** * @param {Array} targetAndSources The first item is target, and the rests are source. * @param {boolean} [overwrite=false] * @return {*} target */ function mergeAll(targetAndSources, overwrite) { var result = targetAndSources[0]; for (var i = 1, len = targetAndSources.length; i < len; i++) { result = merge(result, targetAndSources[i], overwrite); } return result; } /** * @param {*} target * @param {*} source * @memberOf module:zrender/core/util */ function extend(target, source) { for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } return target; } /** * @param {*} target * @param {*} source * @param {boolean} [overlay=false] * @memberOf module:zrender/core/util */ function defaults(target, source, overlay) { for (var key in source) { if (source.hasOwnProperty(key) && (overlay ? source[key] != null : target[key] == null) ) { target[key] = source[key]; } } return target; } var createCanvas = function () { return methods.createCanvas(); }; methods.createCanvas = function () { return document.createElement('canvas'); }; // FIXME var _ctx; function getContext() { if (!_ctx) { // Use util.createCanvas instead of createCanvas // because createCanvas may be overwritten in different environment _ctx = createCanvas().getContext('2d'); } return _ctx; } /** * 查询数组中元素的index * @memberOf module:zrender/core/util */ function indexOf(array, value) { if (array) { if (array.indexOf) { return array.indexOf(value); } for (var i = 0, len = array.length; i < len; i++) { if (array[i] === value) { return i; } } } return -1; } /** * 构造类继承关系 * * @memberOf module:zrender/core/util * @param {Function} clazz 源类 * @param {Function} baseClazz 基类 */ function inherits(clazz, baseClazz) { var clazzPrototype = clazz.prototype; function F() {} F.prototype = baseClazz.prototype; clazz.prototype = new F(); for (var prop in clazzPrototype) { if (clazzPrototype.hasOwnProperty(prop)) { clazz.prototype[prop] = clazzPrototype[prop]; } } clazz.prototype.constructor = clazz; clazz.superClass = baseClazz; } /** * @memberOf module:zrender/core/util * @param {Object|Function} target * @param {Object|Function} sorce * @param {boolean} overlay */ function mixin(target, source, overlay) { target = 'prototype' in target ? target.prototype : target; source = 'prototype' in source ? source.prototype : source; defaults(target, source, overlay); } /** * Consider typed array. * @param {Array|TypedArray} data */ function isArrayLike(data) { if (!data) { return; } if (typeof data === 'string') { return false; } return typeof data.length === 'number'; } /** * 数组或对象遍历 * @memberOf module:zrender/core/util * @param {Object|Array} obj * @param {Function} cb * @param {*} [context] */ function each$1(obj, cb, context) { if (!(obj && cb)) { return; } if (obj.forEach && obj.forEach === nativeForEach) { obj.forEach(cb, context); } else if (obj.length === +obj.length) { for (var i = 0, len = obj.length; i < len; i++) { cb.call(context, obj[i], i, obj); } } else { for (var key in obj) { if (obj.hasOwnProperty(key)) { cb.call(context, obj[key], key, obj); } } } } /** * 数组映射 * @memberOf module:zrender/core/util * @param {Array} obj * @param {Function} cb * @param {*} [context] * @return {Array} */ function map(obj, cb, context) { if (!(obj && cb)) { return; } if (obj.map && obj.map === nativeMap) { return obj.map(cb, context); } else { var result = []; for (var i = 0, len = obj.length; i < len; i++) { result.push(cb.call(context, obj[i], i, obj)); } return result; } } /** * @memberOf module:zrender/core/util * @param {Array} obj * @param {Function} cb * @param {Object} [memo] * @param {*} [context] * @return {Array} */ function reduce(obj, cb, memo, context) { if (!(obj && cb)) { return; } if (obj.reduce && obj.reduce === nativeReduce) { return obj.reduce(cb, memo, context); } else { for (var i = 0, len = obj.length; i < len; i++) { memo = cb.call(context, memo, obj[i], i, obj); } return memo; } } /** * 数组过滤 * @memberOf module:zrender/core/util * @param {Array} obj * @param {Function} cb * @param {*} [context] * @return {Array} */ function filter(obj, cb, context) { if (!(obj && cb)) { return; } if (obj.filter && obj.filter === nativeFilter) { return obj.filter(cb, context); } else { var result = []; for (var i = 0, len = obj.length; i < len; i++) { if (cb.call(context, obj[i], i, obj)) { result.push(obj[i]); } } return result; } } /** * 数组项查找 * @memberOf module:zrender/core/util * @param {Array} obj * @param {Function} cb * @param {*} [context] * @return {*} */ function find(obj, cb, context) { if (!(obj && cb)) { return; } for (var i = 0, len = obj.length; i < len; i++) { if (cb.call(context, obj[i], i, obj)) { return obj[i]; } } } /** * @memberOf module:zrender/core/util * @param {Function} func * @param {*} context * @return {Function} */ function bind(func, context) { var args = nativeSlice.call(arguments, 2); return function () { return func.apply(context, args.concat(nativeSlice.call(arguments))); }; } /** * @memberOf module:zrender/core/util * @param {Function} func * @return {Function} */ function curry(func) { var args = nativeSlice.call(arguments, 1); return function () { return func.apply(this, args.concat(nativeSlice.call(arguments))); }; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isArray(value) { return objToString.call(value) === '[object Array]'; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isFunction$1(value) { return typeof value === 'function'; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isString(value) { return objToString.call(value) === '[object String]'; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isObject$1(value) { // Avoid a V8 JIT bug in Chrome 19-20. // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. var type = typeof value; return type === 'function' || (!!value && type === 'object'); } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isBuiltInObject(value) { return !!BUILTIN_OBJECT[objToString.call(value)]; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isTypedArray(value) { return !!TYPED_ARRAY[objToString.call(value)]; } /** * @memberOf module:zrender/core/util * @param {*} value * @return {boolean} */ function isDom(value) { return typeof value === 'object' && typeof value.nodeType === 'number' && typeof value.ownerDocument === 'object'; } /** * Whether is exactly NaN. Notice isNaN('a') returns true. * @param {*} value * @return {boolean} */ function eqNaN(value) { /* eslint-disable-next-line no-self-compare */ return value !== value; } /** * If value1 is not null, then return value1, otherwise judget rest of values. * Low performance. * @memberOf module:zrender/core/util * @return {*} Final value */ function retrieve(values) { for (var i = 0, len = arguments.length; i < len; i++) { if (arguments[i] != null) { return arguments[i]; } } } function retrieve2(value0, value1) { return value0 != null ? value0 : value1; } function retrieve3(value0, value1, value2) { return value0 != null ? value0 : value1 != null ? value1 : value2; } /** * @memberOf module:zrender/core/util * @param {Array} arr * @param {number} startIndex * @param {number} endIndex * @return {Array} */ function slice() { return Function.call.apply(nativeSlice, arguments); } /** * Normalize css liked array configuration * e.g. * 3 => [3, 3, 3, 3] * [4, 2] => [4, 2, 4, 2] * [4, 3, 2] => [4, 3, 2, 3] * @param {number|Array.} val * @return {Array.} */ function normalizeCssArray(val) { if (typeof (val) === 'number') { return [val, val, val, val]; } var len = val.length; if (len === 2) { // vertical | horizontal return [val[0], val[1], val[0], val[1]]; } else if (len === 3) { // top | horizontal | bottom return [val[0], val[1], val[2], val[1]]; } return val; } /** * @memberOf module:zrender/core/util * @param {boolean} condition * @param {string} message */ function assert$1(condition, message) { if (!condition) { throw new Error(message); } } /** * @memberOf module:zrender/core/util * @param {string} str string to be trimed * @return {string} trimed string */ function trim(str) { if (str == null) { return null; } else if (typeof str.trim === 'function') { return str.trim(); } else { return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); } } var primitiveKey = '__ec_primitive__'; /** * Set an object as primitive to be ignored traversing children in clone or merge */ function setAsPrimitive(obj) { obj[primitiveKey] = true; } function isPrimitive(obj) { return obj[primitiveKey]; } /** * @constructor * @param {Object} obj Only apply `ownProperty`. */ function HashMap(obj) { var isArr = isArray(obj); // Key should not be set on this, otherwise // methods get/set/... may be overrided. this.data = {}; var thisMap = this; (obj instanceof HashMap) ? obj.each(visit) : (obj && each$1(obj, visit)); function visit(value, key) { isArr ? thisMap.set(value, key) : thisMap.set(key, value); } } HashMap.prototype = { constructor: HashMap, // Do not provide `has` method to avoid defining what is `has`. // (We usually treat `null` and `undefined` as the same, different // from ES6 Map). get: function (key) { return this.data.hasOwnProperty(key) ? this.data[key] : null; }, set: function (key, value) { // Comparing with invocation chaining, `return value` is more commonly // used in this case: `var someVal = map.set('a', genVal());` return (this.data[key] = value); }, // Although util.each can be performed on this hashMap directly, user // should not use the exposed keys, who are prefixed. each: function (cb, context) { context !== void 0 && (cb = bind(cb, context)); /* eslint-disable guard-for-in */ for (var key in this.data) { this.data.hasOwnProperty(key) && cb(this.data[key], key); } /* eslint-enable guard-for-in */ }, // Do not use this method if performance sensitive. removeKey: function (key) { delete this.data[key]; } }; function createHashMap(obj) { return new HashMap(obj); } function concatArray(a, b) { var newArray = new a.constructor(a.length + b.length); for (var i = 0; i < a.length; i++) { newArray[i] = a[i]; } var offset = a.length; for (i = 0; i < b.length; i++) { newArray[i + offset] = b[i]; } return newArray; } function noop() {} var zrUtil = (Object.freeze || Object)({ $override: $override, clone: clone, merge: merge, mergeAll: mergeAll, extend: extend, defaults: defaults, createCanvas: createCanvas, getContext: getContext, indexOf: indexOf, inherits: inherits, mixin: mixin, isArrayLike: isArrayLike, each: each$1, map: map, reduce: reduce, filter: filter, find: find, bind: bind, curry: curry, isArray: isArray, isFunction: isFunction$1, isString: isString, isObject: isObject$1, isBuiltInObject: isBuiltInObject, isTypedArray: isTypedArray, isDom: isDom, eqNaN: eqNaN, retrieve: retrieve, retrieve2: retrieve2, retrieve3: retrieve3, slice: slice, normalizeCssArray: normalizeCssArray, assert: assert$1, trim: trim, setAsPrimitive: setAsPrimitive, isPrimitive: isPrimitive, createHashMap: createHashMap, concatArray: concatArray, noop: noop }); /* global Float32Array */ var ArrayCtor = typeof Float32Array === 'undefined' ? Array : Float32Array; /** * 创建一个向量 * @param {number} [x=0] * @param {number} [y=0] * @return {Vector2} */ function create(x, y) { var out = new ArrayCtor(2); if (x == null) { x = 0; } if (y == null) { y = 0; } out[0] = x; out[1] = y; return out; } /** * 复制向量数据 * @param {Vector2} out * @param {Vector2} v * @return {Vector2} */ function copy(out, v) { out[0] = v[0]; out[1] = v[1]; return out; } /** * 克隆一个向量 * @param {Vector2} v * @return {Vector2} */ function clone$1(v) { var out = new ArrayCtor(2); out[0] = v[0]; out[1] = v[1]; return out; } /** * 设置向量的两个项 * @param {Vector2} out * @param {number} a * @param {number} b * @return {Vector2} 结果 */ function set(out, a, b) { out[0] = a; out[1] = b; return out; } /** * 向量相加 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function add(out, v1, v2) { out[0] = v1[0] + v2[0]; out[1] = v1[1] + v2[1]; return out; } /** * 向量缩放后相加 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 * @param {number} a */ function scaleAndAdd(out, v1, v2, a) { out[0] = v1[0] + v2[0] * a; out[1] = v1[1] + v2[1] * a; return out; } /** * 向量相减 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function sub(out, v1, v2) { out[0] = v1[0] - v2[0]; out[1] = v1[1] - v2[1]; return out; } /** * 向量长度 * @param {Vector2} v * @return {number} */ function len(v) { return Math.sqrt(lenSquare(v)); } var length = len; // jshint ignore:line /** * 向量长度平方 * @param {Vector2} v * @return {number} */ function lenSquare(v) { return v[0] * v[0] + v[1] * v[1]; } var lengthSquare = lenSquare; /** * 向量乘法 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function mul(out, v1, v2) { out[0] = v1[0] * v2[0]; out[1] = v1[1] * v2[1]; return out; } /** * 向量除法 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function div(out, v1, v2) { out[0] = v1[0] / v2[0]; out[1] = v1[1] / v2[1]; return out; } /** * 向量点乘 * @param {Vector2} v1 * @param {Vector2} v2 * @return {number} */ function dot(v1, v2) { return v1[0] * v2[0] + v1[1] * v2[1]; } /** * 向量缩放 * @param {Vector2} out * @param {Vector2} v * @param {number} s */ function scale(out, v, s) { out[0] = v[0] * s; out[1] = v[1] * s; return out; } /** * 向量归一化 * @param {Vector2} out * @param {Vector2} v */ function normalize(out, v) { var d = len(v); if (d === 0) { out[0] = 0; out[1] = 0; } else { out[0] = v[0] / d; out[1] = v[1] / d; } return out; } /** * 计算向量间距离 * @param {Vector2} v1 * @param {Vector2} v2 * @return {number} */ function distance(v1, v2) { return Math.sqrt( (v1[0] - v2[0]) * (v1[0] - v2[0]) + (v1[1] - v2[1]) * (v1[1] - v2[1]) ); } var dist = distance; /** * 向量距离平方 * @param {Vector2} v1 * @param {Vector2} v2 * @return {number} */ function distanceSquare(v1, v2) { return (v1[0] - v2[0]) * (v1[0] - v2[0]) + (v1[1] - v2[1]) * (v1[1] - v2[1]); } var distSquare = distanceSquare; /** * 求负向量 * @param {Vector2} out * @param {Vector2} v */ function negate(out, v) { out[0] = -v[0]; out[1] = -v[1]; return out; } /** * 插值两个点 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 * @param {number} t */ function lerp(out, v1, v2, t) { out[0] = v1[0] + t * (v2[0] - v1[0]); out[1] = v1[1] + t * (v2[1] - v1[1]); return out; } /** * 矩阵左乘向量 * @param {Vector2} out * @param {Vector2} v * @param {Vector2} m */ function applyTransform(out, v, m) { var x = v[0]; var y = v[1]; out[0] = m[0] * x + m[2] * y + m[4]; out[1] = m[1] * x + m[3] * y + m[5]; return out; } /** * 求两个向量最小值 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function min(out, v1, v2) { out[0] = Math.min(v1[0], v2[0]); out[1] = Math.min(v1[1], v2[1]); return out; } /** * 求两个向量最大值 * @param {Vector2} out * @param {Vector2} v1 * @param {Vector2} v2 */ function max(out, v1, v2) { out[0] = Math.max(v1[0], v2[0]); out[1] = Math.max(v1[1], v2[1]); return out; } var vector = (Object.freeze || Object)({ create: create, copy: copy, clone: clone$1, set: set, add: add, scaleAndAdd: scaleAndAdd, sub: sub, len: len, length: length, lenSquare: lenSquare, lengthSquare: lengthSquare, mul: mul, div: div, dot: dot, scale: scale, normalize: normalize, distance: distance, dist: dist, distanceSquare: distanceSquare, distSquare: distSquare, negate: negate, lerp: lerp, applyTransform: applyTransform, min: min, max: max }); // TODO Draggable for group // FIXME Draggable on element which has parent rotation or scale function Draggable() { this.on('mousedown', this._dragStart, this); this.on('mousemove', this._drag, this); this.on('mouseup', this._dragEnd, this); // `mosuemove` and `mouseup` can be continue to fire when dragging. // See [Drag outside] in `Handler.js`. So we do not need to trigger // `_dragEnd` when globalout. That would brings better user experience. // this.on('globalout', this._dragEnd, this); // this._dropTarget = null; // this._draggingTarget = null; // this._x = 0; // this._y = 0; } Draggable.prototype = { constructor: Draggable, _dragStart: function (e) { var draggingTarget = e.target; // Find if there is draggable in the ancestor while (draggingTarget && !draggingTarget.draggable) { draggingTarget = draggingTarget.parent; } if (draggingTarget) { this._draggingTarget = draggingTarget; draggingTarget.dragging = true; this._x = e.offsetX; this._y = e.offsetY; this.dispatchToElement(param(draggingTarget, e), 'dragstart', e.event); } }, _drag: function (e) { var draggingTarget = this._draggingTarget; if (draggingTarget) { var x = e.offsetX; var y = e.offsetY; var dx = x - this._x; var dy = y - this._y; this._x = x; this._y = y; draggingTarget.drift(dx, dy, e); this.dispatchToElement(param(draggingTarget, e), 'drag', e.event); var dropTarget = this.findHover(x, y, draggingTarget).target; var lastDropTarget = this._dropTarget; this._dropTarget = dropTarget; if (draggingTarget !== dropTarget) { if (lastDropTarget && dropTarget !== lastDropTarget) { this.dispatchToElement(param(lastDropTarget, e), 'dragleave', e.event); } if (dropTarget && dropTarget !== lastDropTarget) { this.dispatchToElement(param(dropTarget, e), 'dragenter', e.event); } } } }, _dragEnd: function (e) { var draggingTarget = this._draggingTarget; if (draggingTarget) { draggingTarget.dragging = false; } this.dispatchToElement(param(draggingTarget, e), 'dragend', e.event); if (this._dropTarget) { this.dispatchToElement(param(this._dropTarget, e), 'drop', e.event); } this._draggingTarget = null; this._dropTarget = null; } }; function param(target, e) { return {target: target, topTarget: e && e.topTarget}; } /** * Event Mixin * @module zrender/mixin/Eventful * @author Kener (@Kener-林峰, kener.linfeng@gmail.com) * pissang (https://www.github.com/pissang) */ var arrySlice = Array.prototype.slice; /** * Event dispatcher. * * @alias module:zrender/mixin/Eventful * @constructor * @param {Object} [eventProcessor] The object eventProcessor is the scope when * `eventProcessor.xxx` called. * @param {Function} [eventProcessor.normalizeQuery] * param: {string|Object} Raw query. * return: {string|Object} Normalized query. * @param {Function} [eventProcessor.filter] Event will be dispatched only * if it returns `true`. * param: {string} eventType * param: {string|Object} query * return: {boolean} * @param {Function} [eventProcessor.afterTrigger] Called after all handlers called. * param: {string} eventType */ var Eventful = function (eventProcessor) { this._$handlers = {}; this._$eventProcessor = eventProcessor; }; Eventful.prototype = { constructor: Eventful, /** * The handler can only be triggered once, then removed. * * @param {string} event The event name. * @param {string|Object} [query] Condition used on event filter. * @param {Function} handler The event handler. * @param {Object} context */ one: function (event, query, handler, context) { return on(this, event, query, handler, context, true); }, /** * Bind a handler. * * @param {string} event The event name. * @param {string|Object} [query] Condition used on event filter. * @param {Function} handler The event handler. * @param {Object} [context] */ on: function (event, query, handler, context) { return on(this, event, query, handler, context, false); }, /** * Whether any handler has bound. * * @param {string} event * @return {boolean} */ isSilent: function (event) { var _h = this._$handlers; return !_h[event] || !_h[event].length; }, /** * Unbind a event. * * @param {string} [event] The event name. * If no `event` input, "off" all listeners. * @param {Function} [handler] The event handler. * If no `handler` input, "off" all listeners of the `event`. */ off: function (event, handler) { var _h = this._$handlers; if (!event) { this._$handlers = {}; return this; } if (handler) { if (_h[event]) { var newList = []; for (var i = 0, l = _h[event].length; i < l; i++) { if (_h[event][i].h !== handler) { newList.push(_h[event][i]); } } _h[event] = newList; } if (_h[event] && _h[event].length === 0) { delete _h[event]; } } else { delete _h[event]; } return this; }, /** * Dispatch a event. * * @param {string} type The event name. */ trigger: function (type) { var _h = this._$handlers[type]; var eventProcessor = this._$eventProcessor; if (_h) { var args = arguments; var argLen = args.length; if (argLen > 3) { args = arrySlice.call(args, 1); } var len = _h.length; for (var i = 0; i < len;) { var hItem = _h[i]; if (eventProcessor && eventProcessor.filter && hItem.query != null && !eventProcessor.filter(type, hItem.query) ) { i++; continue; } // Optimize advise from backbone switch (argLen) { case 1: hItem.h.call(hItem.ctx); break; case 2: hItem.h.call(hItem.ctx, args[1]); break; case 3: hItem.h.call(hItem.ctx, args[1], args[2]); break; default: // have more than 2 given arguments hItem.h.apply(hItem.ctx, args); break; } if (hItem.one) { _h.splice(i, 1); len--; } else { i++; } } } eventProcessor && eventProcessor.afterTrigger && eventProcessor.afterTrigger(type); return this; }, /** * Dispatch a event with context, which is specified at the last parameter. * * @param {string} type The event name. */ triggerWithContext: function (type) { var _h = this._$handlers[type]; var eventProcessor = this._$eventProcessor; if (_h) { var args = arguments; var argLen = args.length; if (argLen > 4) { args = arrySlice.call(args, 1, args.length - 1); } var ctx = args[args.length - 1]; var len = _h.length; for (var i = 0; i < len;) { var hItem = _h[i]; if (eventProcessor && eventProcessor.filter && hItem.query != null && !eventProcessor.filter(type, hItem.query) ) { i++; continue; } // Optimize advise from backbone switch (argLen) { case 1: hItem.h.call(ctx); break; case 2: hItem.h.call(ctx, args[1]); break; case 3: hItem.h.call(ctx, args[1], args[2]); break; default: // have more than 2 given arguments hItem.h.apply(ctx, args); break; } if (hItem.one) { _h.splice(i, 1); len--; } else { i++; } } } eventProcessor && eventProcessor.afterTrigger && eventProcessor.afterTrigger(type); return this; } }; function normalizeQuery(host, query) { var eventProcessor = host._$eventProcessor; if (query != null && eventProcessor && eventProcessor.normalizeQuery) { query = eventProcessor.normalizeQuery(query); } return query; } function on(eventful, event, query, handler, context, isOnce) { var _h = eventful._$handlers; if (typeof query === 'function') { context = handler; handler = query; query = null; } if (!handler || !event) { return eventful; } query = normalizeQuery(eventful, query); if (!_h[event]) { _h[event] = []; } for (var i = 0; i < _h[event].length; i++) { if (_h[event][i].h === handler) { return eventful; } } var wrap = { h: handler, one: isOnce, query: query, ctx: context || eventful, // FIXME // Do not publish this feature util it is proved that it makes sense. callAtLast: handler.zrEventfulCallAtLast }; var lastIndex = _h[event].length - 1; var lastWrap = _h[event][lastIndex]; (lastWrap && lastWrap.callAtLast) ? _h[event].splice(lastIndex, 0, wrap) : _h[event].push(wrap); return eventful; } /** * The algoritm is learnt from * https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/ * And we made some optimization for matrix inversion. * Other similar approaches: * "cv::getPerspectiveTransform", "Direct Linear Transformation". */ var LN2 = Math.log(2); function determinant(rows, rank, rowStart, rowMask, colMask, detCache) { var cacheKey = rowMask + '-' + colMask; var fullRank = rows.length; if (detCache.hasOwnProperty(cacheKey)) { return detCache[cacheKey]; } if (rank === 1) { // In this case the colMask must be like: `11101111`. We can find the place of `0`. var colStart = Math.round(Math.log(((1 << fullRank) - 1) & ~colMask) / LN2); return rows[rowStart][colStart]; } var subRowMask = rowMask | (1 << rowStart); var subRowStart = rowStart + 1; while (rowMask & (1 << subRowStart)) { subRowStart++; } var sum = 0; for (var j = 0, colLocalIdx = 0; j < fullRank; j++) { var colTag = 1 << j; if (!(colTag & colMask)) { sum += (colLocalIdx % 2 ? -1 : 1) * rows[rowStart][j] // det(subMatrix(0, j)) * determinant(rows, rank - 1, subRowStart, subRowMask, colMask | colTag, detCache); colLocalIdx++; } } detCache[cacheKey] = sum; return sum; } /** * Usage: * ```js * var transformer = buildTransformer( * [10, 44, 100, 44, 100, 300, 10, 300], * [50, 54, 130, 14, 140, 330, 14, 220] * ); * var out = []; * transformer && transformer([11, 33], out); * ``` * * Notice: `buildTransformer` may take more than 10ms in some Android device. * * @param {Array.} src source four points, [x0, y0, x1, y1, x2, y2, x3, y3] * @param {Array.} dest destination four points, [x0, y0, x1, y1, x2, y2, x3, y3] * @return {Function} transformer If fail, return null/undefined. */ function buildTransformer(src, dest) { var mA = [ [src[0], src[1], 1, 0, 0, 0, -dest[0] * src[0], -dest[0] * src[1]], [0, 0, 0, src[0], src[1], 1, -dest[1] * src[0], -dest[1] * src[1]], [src[2], src[3], 1, 0, 0, 0, -dest[2] * src[2], -dest[2] * src[3]], [0, 0, 0, src[2], src[3], 1, -dest[3] * src[2], -dest[3] * src[3]], [src[4], src[5], 1, 0, 0, 0, -dest[4] * src[4], -dest[4] * src[5]], [0, 0, 0, src[4], src[5], 1, -dest[5] * src[4], -dest[5] * src[5]], [src[6], src[7], 1, 0, 0, 0, -dest[6] * src[6], -dest[6] * src[7]], [0, 0, 0, src[6], src[7], 1, -dest[7] * src[6], -dest[7] * src[7]] ]; var detCache = {}; var det = determinant(mA, 8, 0, 0, 0, detCache); if (det === 0) { // can not make transformer when and only when // any three of the markers are collinear. return; } // `invert(mA) * dest`, that is, `adj(mA) / det * dest`. var vh = []; for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { vh[j] == null && (vh[j] = 0); vh[j] += ((i + j) % 2 ? -1 : 1) // det(subMatrix(i, j)) * determinant(mA, 7, i === 0 ? 1 : 0, 1 << i, 1 << j, detCache) / det * dest[i]; } } return function (out, srcPointX, srcPointY) { var pk = srcPointX * vh[6] + srcPointY * vh[7] + 1; out[0] = (srcPointX * vh[0] + srcPointY * vh[1] + vh[2]) / pk; out[1] = (srcPointX * vh[3] + srcPointY * vh[4] + vh[5]) / pk; }; } var EVENT_SAVED_PROP = '___zrEVENTSAVED'; var _calcOut$1 = []; /** * Transform "local coord" from `elFrom` to `elTarget`. * "local coord": the coord based on the input `el`. The origin point is at * the position of "left: 0; top: 0;" in the `el`. * * Support when CSS transform is used. * * Having the `out` (that is, `[outX, outY]`), we can create an DOM element * and set the CSS style as "left: outX; top: outY;" and append it to `elTarge` * to locate the element. * * For example, this code below positions a child of `document.body` on the event * point, no matter whether `body` has `margin`/`paddin`/`transfrom`/... : * ```js * transformLocalCoord(out, container, document.body, event.offsetX, event.offsetY); * if (!eqNaN(out[0])) { * // Then locate the tip element on the event point. * var tipEl = document.createElement('div'); * tipEl.style.cssText = 'position: absolute; left:' + out[0] + ';top:' + out[1] + ';'; * document.body.appendChild(tipEl); * } * ``` * * Notice: In some env this method is not supported. If called, `out` will be `[NaN, NaN]`. * * @param {Array.} out [inX: number, inY: number] The output.. * If can not transform, `out` will not be modified but return `false`. * @param {HTMLElement} elFrom The `[inX, inY]` is based on elFrom. * @param {HTMLElement} elTarget The `out` is based on elTarget. * @param {number} inX * @param {number} inY * @return {boolean} Whether transform successfully. */ function transformLocalCoord(out, elFrom, elTarget, inX, inY) { return transformCoordWithViewport(_calcOut$1, elFrom, inX, inY, true) && transformCoordWithViewport(out, elTarget, _calcOut$1[0], _calcOut$1[1]); } /** * Transform between a "viewport coord" and a "local coord". * "viewport coord": the coord based on the left-top corner of the viewport * of the browser. * "local coord": the coord based on the input `el`. The origin point is at * the position of "left: 0; top: 0;" in the `el`. * * Support the case when CSS transform is used on el. * * @param {Array.} out [inX: number, inY: number] The output. If `inverse: false`, * it represents "local coord", otherwise "vireport coord". * If can not transform, `out` will not be modified but return `false`. * @param {HTMLElement} el The "local coord" is based on the `el`, see comment above. * @param {number} inX If `inverse: false`, * it represents "vireport coord", otherwise "local coord". * @param {number} inY If `inverse: false`, * it represents "vireport coord", otherwise "local coord". * @param {boolean} [inverse=false] * `true`: from "viewport coord" to "local coord". * `false`: from "local coord" to "viewport coord". * @return {boolean} Whether transform successfully. */ function transformCoordWithViewport(out, el, inX, inY, inverse) { if (el.getBoundingClientRect && env$1.domSupported && !isCanvasEl(el)) { var saved = el[EVENT_SAVED_PROP] || (el[EVENT_SAVED_PROP] = {}); var markers = prepareCoordMarkers(el, saved); var transformer = preparePointerTransformer(markers, saved, inverse); if (transformer) { transformer(out, inX, inY); return true; } } return false; } function prepareCoordMarkers(el, saved) { var markers = saved.markers; if (markers) { return markers; } markers = saved.markers = []; var propLR = ['left', 'right']; var propTB = ['top', 'bottom']; for (var i = 0; i < 4; i++) { var marker = document.createElement('div'); var stl = marker.style; var idxLR = i % 2; var idxTB = (i >> 1) % 2; stl.cssText = [ 'position: absolute', 'visibility: hidden', 'padding: 0', 'margin: 0', 'border-width: 0', 'user-select: none', 'width:0', 'height:0', // 'width: 5px', // 'height: 5px', propLR[idxLR] + ':0', propTB[idxTB] + ':0', propLR[1 - idxLR] + ':auto', propTB[1 - idxTB] + ':auto', '' ].join('!important;'); el.appendChild(marker); markers.push(marker); } return markers; } function preparePointerTransformer(markers, saved, inverse) { var transformerName = inverse ? 'invTrans' : 'trans'; var transformer = saved[transformerName]; var oldSrcCoords = saved.srcCoords; var oldCoordTheSame = true; var srcCoords = []; var destCoords = []; for (var i = 0; i < 4; i++) { var rect = markers[i].getBoundingClientRect(); var ii = 2 * i; var x = rect.left; var y = rect.top; srcCoords.push(x, y); oldCoordTheSame = oldCoordTheSame && oldSrcCoords && x === oldSrcCoords[ii] && y === oldSrcCoords[ii + 1]; destCoords.push(markers[i].offsetLeft, markers[i].offsetTop); } // Cache to avoid time consuming of `buildTransformer`. return (oldCoordTheSame && transformer) ? transformer : ( saved.srcCoords = srcCoords, saved[transformerName] = inverse ? buildTransformer(destCoords, srcCoords) : buildTransformer(srcCoords, destCoords) ); } function isCanvasEl(el) { return el.nodeName.toUpperCase() === 'CANVAS'; } /** * Utilities for mouse or touch events. */ var isDomLevel2 = (typeof window !== 'undefined') && !!window.addEventListener; var MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; var _calcOut = []; /** * Get the `zrX` and `zrY`, which are relative to the top-left of * the input `el`. * CSS transform (2D & 3D) is supported. * * The strategy to fetch the coords: * + If `calculate` is not set as `true`, users of this method should * ensure that `el` is the same or the same size & location as `e.target`. * Otherwise the result coords are probably not expected. Because we * firstly try to get coords from e.offsetX/e.offsetY. * + If `calculate` is set as `true`, the input `el` can be any element * and we force to calculate the coords based on `el`. * + The input `el` should be positionable (not position:static). * * The force `calculate` can be used in case like: * When mousemove event triggered on ec tooltip, `e.target` is not `el`(zr painter.dom). * * @param {HTMLElement} el DOM element. * @param {Event} e Mouse event or touch event. * @param {Object} out Get `out.zrX` and `out.zrY` as the result. * @param {boolean} [calculate=false] Whether to force calculate * the coordinates but not use ones provided by browser. */ function clientToLocal(el, e, out, calculate) { out = out || {}; // According to the W3C Working Draft, offsetX and offsetY should be relative // to the padding edge of the target element. The only browser using this convention // is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does // not support the properties. // (see http://www.jacklmoore.com/notes/mouse-position/) // In zr painter.dom, padding edge equals to border edge. if (calculate || !env$1.canvasSupported) { calculateZrXY(el, e, out); } // Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned // ancestor element, so we should make sure el is positioned (e.g., not position:static). // BTW1, Webkit don't return the same results as FF in non-simple cases (like add // zoom-factor, overflow / opacity layers, transforms ...) // BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d. // // BTW3, In ff, offsetX/offsetY is always 0. else if (env$1.browser.firefox && e.layerX != null && e.layerX !== e.offsetX) { out.zrX = e.layerX; out.zrY = e.layerY; } // For IE6+, chrome, safari, opera. (When will ff support offsetX?) else if (e.offsetX != null) { out.zrX = e.offsetX; out.zrY = e.offsetY; } // For some other device, e.g., IOS safari. else { calculateZrXY(el, e, out); } return out; } function calculateZrXY(el, e, out) { // BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect. if (env$1.domSupported && el.getBoundingClientRect) { var ex = e.clientX; var ey = e.clientY; if (isCanvasEl(el)) { // Original approach, which do not support CSS transform. // marker can not be locationed in a canvas container // (getBoundingClientRect is always 0). We do not support // that input a pre-created canvas to zr while using css // transform in iOS. var box = el.getBoundingClientRect(); out.zrX = ex - box.left; out.zrY = ey - box.top; return; } else { if (transformCoordWithViewport(_calcOut, el, ex, ey)) { out.zrX = _calcOut[0]; out.zrY = _calcOut[1]; return; } } } out.zrX = out.zrY = 0; } /** * Find native event compat for legency IE. * Should be called at the begining of a native event listener. * * @param {Event} [e] Mouse event or touch event or pointer event. * For lagency IE, we use `window.event` is used. * @return {Event} The native event. */ function getNativeEvent(e) { return e || window.event; } /** * Normalize the coordinates of the input event. * * Get the `e.zrX` and `e.zrY`, which are relative to the top-left of * the input `el`. * Get `e.zrDelta` if using mouse wheel. * Get `e.which`, see the comment inside this function. * * Do not calculate repeatly if `zrX` and `zrY` already exist. * * Notice: see comments in `clientToLocal`. check the relationship * between the result coords and the parameters `el` and `calculate`. * * @param {HTMLElement} el DOM element. * @param {Event} [e] See `getNativeEvent`. * @param {boolean} [calculate=false] Whether to force calculate * the coordinates but not use ones provided by browser. * @return {UIEvent} The normalized native UIEvent. */ function normalizeEvent(el, e, calculate) { e = getNativeEvent(e); if (e.zrX != null) { return e; } var eventType = e.type; var isTouch = eventType && eventType.indexOf('touch') >= 0; if (!isTouch) { clientToLocal(el, e, e, calculate); e.zrDelta = (e.wheelDelta) ? e.wheelDelta / 120 : -(e.detail || 0) / 3; } else { var touch = eventType !== 'touchend' ? e.targetTouches[0] : e.changedTouches[0]; touch && clientToLocal(el, touch, e, calculate); } // Add which for click: 1 === left; 2 === middle; 3 === right; otherwise: 0; // See jQuery: https://github.com/jquery/jquery/blob/master/src/event.js // If e.which has been defined, it may be readonly, // see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which var button = e.button; if (e.which == null && button !== undefined && MOUSE_EVENT_REG.test(e.type)) { e.which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0))); } // [Caution]: `e.which` from browser is not always reliable. For example, // when press left button and `mousemove (pointermove)` in Edge, the `e.which` // is 65536 and the `e.button` is -1. But the `mouseup (pointerup)` and // `mousedown (pointerdown)` is the same as Chrome does. return e; } /** * @param {HTMLElement} el * @param {string} name * @param {Function} handler * @param {Object|boolean} opt If boolean, means `opt.capture` * @param {boolean} [opt.capture=false] * @param {boolean} [opt.passive=false] */ function addEventListener(el, name, handler, opt) { if (isDomLevel2) { // Reproduct the console warning: // [Violation] Added non-passive event listener to a scroll-blocking event. // Consider marking event handler as 'passive' to make the page more responsive. // Just set console log level: verbose in chrome dev tool. // then the warning log will be printed when addEventListener called. // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md // We have not yet found a neat way to using passive. Because in zrender the dom event // listener delegate all of the upper events of element. Some of those events need // to prevent default. For example, the feature `preventDefaultMouseMove` of echarts. // Before passive can be adopted, these issues should be considered: // (1) Whether and how a zrender user specifies an event listener passive. And by default, // passive or not. // (2) How to tread that some zrender event listener is passive, and some is not. If // we use other way but not preventDefault of mousewheel and touchmove, browser // compatibility should be handled. // var opts = (env.passiveSupported && name === 'mousewheel') // ? {passive: true} // // By default, the third param of el.addEventListener is `capture: false`. // : void 0; // el.addEventListener(name, handler /* , opts */); el.addEventListener(name, handler, opt); } else { // For simplicity, do not implement `setCapture` for IE9-. el.attachEvent('on' + name, handler); } } /** * Parameter are the same as `addEventListener`. * * Notice that if a listener is registered twice, one with capture and one without, * remove each one separately. Removal of a capturing listener does not affect a * non-capturing version of the same listener, and vice versa. */ function removeEventListener(el, name, handler, opt) { if (isDomLevel2) { el.removeEventListener(name, handler, opt); } else { el.detachEvent('on' + name, handler); } } /** * preventDefault and stopPropagation. * Notice: do not use this method in zrender. It can only be * used by upper applications if necessary. * * @param {Event} e A mouse or touch event. */ var stop = isDomLevel2 ? function (e) { e.preventDefault(); e.stopPropagation(); e.cancelBubble = true; } : function (e) { e.returnValue = false; e.cancelBubble = true; }; /** * This method only works for mouseup and mousedown. The functionality is restricted * for fault tolerance, See the `e.which` compatibility above. * * @param {MouseEvent} e * @return {boolean} */ function isMiddleOrRightButtonOnMouseUpDown(e) { return e.which === 2 || e.which === 3; } /** * To be removed. * @deprecated */ /** * Only implements needed gestures for mobile. */ var GestureMgr = function () { /** * @private * @type {Array.} */ this._track = []; }; GestureMgr.prototype = { constructor: GestureMgr, recognize: function (event, target, root) { this._doTrack(event, target, root); return this._recognize(event); }, clear: function () { this._track.length = 0; return this; }, _doTrack: function (event, target, root) { var touches = event.touches; if (!touches) { return; } var trackItem = { points: [], touches: [], target: target, event: event }; for (var i = 0, len = touches.length; i < len; i++) { var touch = touches[i]; var pos = clientToLocal(root, touch, {}); trackItem.points.push([pos.zrX, pos.zrY]); trackItem.touches.push(touch); } this._track.push(trackItem); }, _recognize: function (event) { for (var eventName in recognizers) { if (recognizers.hasOwnProperty(eventName)) { var gestureInfo = recognizers[eventName](this._track, event); if (gestureInfo) { return gestureInfo; } } } } }; function dist$1(pointPair) { var dx = pointPair[1][0] - pointPair[0][0]; var dy = pointPair[1][1] - pointPair[0][1]; return Math.sqrt(dx * dx + dy * dy); } function center(pointPair) { return [ (pointPair[0][0] + pointPair[1][0]) / 2, (pointPair[0][1] + pointPair[1][1]) / 2 ]; } var recognizers = { pinch: function (track, event) { var trackLen = track.length; if (!trackLen) { return; } var pinchEnd = (track[trackLen - 1] || {}).points; var pinchPre = (track[trackLen - 2] || {}).points || pinchEnd; if (pinchPre && pinchPre.length > 1 && pinchEnd && pinchEnd.length > 1 ) { var pinchScale = dist$1(pinchEnd) / dist$1(pinchPre); !isFinite(pinchScale) && (pinchScale = 1); event.pinchScale = pinchScale; var pinchCenter = center(pinchEnd); event.pinchX = pinchCenter[0]; event.pinchY = pinchCenter[1]; return { type: 'pinch', target: track[0].target, event: event }; } } // Only pinch currently. }; /** * [The interface between `Handler` and `HandlerProxy`]: * * The default `HandlerProxy` only support the common standard web environment * (e.g., standalone browser, headless browser, embed browser in mobild APP, ...). * But `HandlerProxy` can be replaced to support more non-standard environment * (e.g., mini app), or to support more feature that the default `HandlerProxy` * not provided (like echarts-gl did). * So the interface between `Handler` and `HandlerProxy` should be stable. Do not * make break changes util inevitable. The interface include the public methods * of `Handler` and the events listed in `handlerNames` below, by which `HandlerProxy` * drives `Handler`. */ /** * [Drag outside]: * * That is, triggering `mousemove` and `mouseup` event when the pointer is out of the * zrender area when dragging. That is important for the improvement of the user experience * when dragging something near the boundary without being terminated unexpectedly. * * We originally consider to introduce new events like `pagemovemove` and `pagemouseup` * to resolve this issue. But some drawbacks of it is described in * https://github.com/ecomfe/zrender/pull/536#issuecomment-560286899 * * Instead, we referenced the specifications: * https://www.w3.org/TR/touch-events/#the-touchmove-event * https://www.w3.org/TR/2014/WD-DOM-Level-3-Events-20140925/#event-type-mousemove * where the the mousemove/touchmove can be continue to fire if the user began a drag * operation and the pointer has left the boundary. (for the mouse event, browsers * only do it on `document` and when the pointer has left the boundary of the browser.) * * So the default `HandlerProxy` supports this feature similarly: if it is in the dragging * state (see `pointerCapture` in `HandlerProxy`), the `mousemove` and `mouseup` continue * to fire until release the pointer. That is implemented by listen to those event on * `document`. * If we implement some other `HandlerProxy` only for touch device, that would be easier. * The touch event support this feature by default. * * Note: * There might be some cases that the mouse event can not be * received on `document`. For example, * (A) `useCapture` is not supported and some user defined event listeners on the ancestor * of zr dom throw Error . * (B) `useCapture` is not supported Some user defined event listeners on the ancestor of * zr dom call `stopPropagation`. * In these cases, the `mousemove` event might be keep triggered event * if the mouse is released. We try to reduce the side-effect in those cases. * That is, do nothing (especially, `findHover`) in those cases. See `isOutsideBoundary`. * * Note: * If `HandlerProxy` listens to `document` with `useCapture`, `HandlerProxy` needs to * make sure `stopPropagation` and `preventDefault` doing nothing if and only if the event * target is not zrender dom. Becuase it is dangerous to enable users to call them in * `document` capture phase to prevent the propagation to any listener of the webpage. * But they are needed to work when the pointer inside the zrender dom. */ var SILENT = 'silent'; function makeEventPacket(eveType, targetInfo, event) { return { type: eveType, event: event, // target can only be an element that is not silent. target: targetInfo.target, // topTarget can be a silent element. topTarget: targetInfo.topTarget, cancelBubble: false, offsetX: event.zrX, offsetY: event.zrY, gestureEvent: event.gestureEvent, pinchX: event.pinchX, pinchY: event.pinchY, pinchScale: event.pinchScale, wheelDelta: event.zrDelta, zrByTouch: event.zrByTouch, which: event.which, stop: stopEvent }; } function stopEvent() { stop(this.event); } function EmptyProxy() {} EmptyProxy.prototype.dispose = function () {}; var handlerNames = [ 'click', 'dblclick', 'mousewheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu' ]; /** * @alias module:zrender/Handler * @constructor * @extends module:zrender/mixin/Eventful * @param {module:zrender/Storage} storage Storage instance. * @param {module:zrender/Painter} painter Painter instance. * @param {module:zrender/dom/HandlerProxy} proxy HandlerProxy instance. * @param {HTMLElement} painterRoot painter.root (not painter.getViewportRoot()). */ var Handler = function (storage, painter, proxy, painterRoot) { Eventful.call(this); this.storage = storage; this.painter = painter; this.painterRoot = painterRoot; proxy = proxy || new EmptyProxy(); /** * Proxy of event. can be Dom, WebGLSurface, etc. */ this.proxy = null; /** * {target, topTarget, x, y} * @private * @type {Object} */ this._hovered = {}; /** * @private * @type {Date} */ this._lastTouchMoment; /** * @private * @type {number} */ this._lastX; /** * @private * @type {number} */ this._lastY; /** * @private * @type {module:zrender/core/GestureMgr} */ this._gestureMgr; Draggable.call(this); this.setHandlerProxy(proxy); }; Handler.prototype = { constructor: Handler, setHandlerProxy: function (proxy) { if (this.proxy) { this.proxy.dispose(); } if (proxy) { each$1(handlerNames, function (name) { proxy.on && proxy.on(name, this[name], this); }, this); // Attach handler proxy.handler = this; } this.proxy = proxy; }, mousemove: function (event) { var x = event.zrX; var y = event.zrY; var isOutside = isOutsideBoundary(this, x, y); var lastHovered = this._hovered; var lastHoveredTarget = lastHovered.target; // If lastHoveredTarget is removed from zr (detected by '__zr') by some API call // (like 'setOption' or 'dispatchAction') in event handlers, we should find // lastHovered again here. Otherwise 'mouseout' can not be triggered normally. // See #6198. if (lastHoveredTarget && !lastHoveredTarget.__zr) { lastHovered = this.findHover(lastHovered.x, lastHovered.y); lastHoveredTarget = lastHovered.target; } var hovered = this._hovered = isOutside ? {x: x, y: y} : this.findHover(x, y); var hoveredTarget = hovered.target; var proxy = this.proxy; proxy.setCursor && proxy.setCursor(hoveredTarget ? hoveredTarget.cursor : 'default'); // Mouse out on previous hovered element if (lastHoveredTarget && hoveredTarget !== lastHoveredTarget) { this.dispatchToElement(lastHovered, 'mouseout', event); } // Mouse moving on one element this.dispatchToElement(hovered, 'mousemove', event); // Mouse over on a new element if (hoveredTarget && hoveredTarget !== lastHoveredTarget) { this.dispatchToElement(hovered, 'mouseover', event); } }, mouseout: function (event) { var eventControl = event.zrEventControl; var zrIsToLocalDOM = event.zrIsToLocalDOM; if (eventControl !== 'only_globalout') { this.dispatchToElement(this._hovered, 'mouseout', event); } if (eventControl !== 'no_globalout') { // FIXME: if the pointer moving from the extra doms to realy "outside", // the `globalout` should have been triggered. But currently not. !zrIsToLocalDOM && this.trigger('globalout', {type: 'globalout', event: event}); } }, /** * Resize */ resize: function (event) { this._hovered = {}; }, /** * Dispatch event * @param {string} eventName * @param {event=} eventArgs */ dispatch: function (eventName, eventArgs) { var handler = this[eventName]; handler && handler.call(this, eventArgs); }, /** * Dispose */ dispose: function () { this.proxy.dispose(); this.storage = this.proxy = this.painter = null; }, /** * 设置默认的cursor style * @param {string} [cursorStyle='default'] 例如 crosshair */ setCursorStyle: function (cursorStyle) { var proxy = this.proxy; proxy.setCursor && proxy.setCursor(cursorStyle); }, /** * 事件分发代理 * * @private * @param {Object} targetInfo {target, topTarget} 目标图形元素 * @param {string} eventName 事件名称 * @param {Object} event 事件对象 */ dispatchToElement: function (targetInfo, eventName, event) { targetInfo = targetInfo || {}; var el = targetInfo.target; if (el && el.silent) { return; } var eventHandler = 'on' + eventName; var eventPacket = makeEventPacket(eventName, targetInfo, event); while (el) { el[eventHandler] && (eventPacket.cancelBubble = el[eventHandler].call(el, eventPacket)); el.trigger(eventName, eventPacket); el = el.parent; if (eventPacket.cancelBubble) { break; } } if (!eventPacket.cancelBubble) { // 冒泡到顶级 zrender 对象 this.trigger(eventName, eventPacket); // 分发事件到用户自定义层 // 用户有可能在全局 click 事件中 dispose,所以需要判断下 painter 是否存在 this.painter && this.painter.eachOtherLayer(function (layer) { if (typeof (layer[eventHandler]) === 'function') { layer[eventHandler].call(layer, eventPacket); } if (layer.trigger) { layer.trigger(eventName, eventPacket); } }); } }, /** * @private * @param {number} x * @param {number} y * @param {module:zrender/graphic/Displayable} exclude * @return {model:zrender/Element} * @method */ findHover: function (x, y, exclude) { var list = this.storage.getDisplayList(); var out = {x: x, y: y}; for (var i = list.length - 1; i >= 0; i--) { var hoverCheckResult; if (list[i] !== exclude // getDisplayList may include ignored item in VML mode && !list[i].ignore && (hoverCheckResult = isHover(list[i], x, y)) ) { !out.topTarget && (out.topTarget = list[i]); if (hoverCheckResult !== SILENT) { out.target = list[i]; break; } } } return out; }, processGesture: function (event, stage) { if (!this._gestureMgr) { this._gestureMgr = new GestureMgr(); } var gestureMgr = this._gestureMgr; stage === 'start' && gestureMgr.clear(); var gestureInfo = gestureMgr.recognize( event, this.findHover(event.zrX, event.zrY, null).target, this.proxy.dom ); stage === 'end' && gestureMgr.clear(); // Do not do any preventDefault here. Upper application do that if necessary. if (gestureInfo) { var type = gestureInfo.type; event.gestureEvent = type; this.dispatchToElement({target: gestureInfo.target}, type, gestureInfo.event); } } }; // Common handlers each$1(['click', 'mousedown', 'mouseup', 'mousewheel', 'dblclick', 'contextmenu'], function (name) { Handler.prototype[name] = function (event) { var x = event.zrX; var y = event.zrY; var isOutside = isOutsideBoundary(this, x, y); var hovered; var hoveredTarget; if (name !== 'mouseup' || !isOutside) { // Find hover again to avoid click event is dispatched manually. Or click is triggered without mouseover hovered = this.findHover(x, y); hoveredTarget = hovered.target; } if (name === 'mousedown') { this._downEl = hoveredTarget; this._downPoint = [event.zrX, event.zrY]; // In case click triggered before mouseup this._upEl = hoveredTarget; } else if (name === 'mouseup') { this._upEl = hoveredTarget; } else if (name === 'click') { if (this._downEl !== this._upEl // Original click event is triggered on the whole canvas element, // including the case that `mousedown` - `mousemove` - `mouseup`, // which should be filtered, otherwise it will bring trouble to // pan and zoom. || !this._downPoint // Arbitrary value || dist(this._downPoint, [event.zrX, event.zrY]) > 4 ) { return; } this._downPoint = null; } this.dispatchToElement(hovered, name, event); }; }); function isHover(displayable, x, y) { if (displayable[displayable.rectHover ? 'rectContain' : 'contain'](x, y)) { var el = displayable; var isSilent; while (el) { // If clipped by ancestor. // FIXME: If clipPath has neither stroke nor fill, // el.clipPath.contain(x, y) will always return false. if (el.clipPath && !el.clipPath.contain(x, y)) { return false; } if (el.silent) { isSilent = true; } el = el.parent; } return isSilent ? SILENT : true; } return false; } /** * See [Drag outside]. */ function isOutsideBoundary(handlerInstance, x, y) { var painter = handlerInstance.painter; return x < 0 || x > painter.getWidth() || y < 0 || y > painter.getHeight(); } mixin(Handler, Eventful); mixin(Handler, Draggable); /** * 3x2矩阵操作类 * @exports zrender/tool/matrix */ /* global Float32Array */ var ArrayCtor$1 = typeof Float32Array === 'undefined' ? Array : Float32Array; /** * Create a identity matrix. * @return {Float32Array|Array.} */ function create$1() { var out = new ArrayCtor$1(6); identity(out); return out; } /** * 设置矩阵为单位矩阵 * @param {Float32Array|Array.} out */ function identity(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 0; return out; } /** * 复制矩阵 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} m */ function copy$1(out, m) { out[0] = m[0]; out[1] = m[1]; out[2] = m[2]; out[3] = m[3]; out[4] = m[4]; out[5] = m[5]; return out; } /** * 矩阵相乘 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} m1 * @param {Float32Array|Array.} m2 */ function mul$1(out, m1, m2) { // Consider matrix.mul(m, m2, m); // where out is the same as m2. // So use temp variable to escape error. var out0 = m1[0] * m2[0] + m1[2] * m2[1]; var out1 = m1[1] * m2[0] + m1[3] * m2[1]; var out2 = m1[0] * m2[2] + m1[2] * m2[3]; var out3 = m1[1] * m2[2] + m1[3] * m2[3]; var out4 = m1[0] * m2[4] + m1[2] * m2[5] + m1[4]; var out5 = m1[1] * m2[4] + m1[3] * m2[5] + m1[5]; out[0] = out0; out[1] = out1; out[2] = out2; out[3] = out3; out[4] = out4; out[5] = out5; return out; } /** * 平移变换 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} a * @param {Float32Array|Array.} v */ function translate(out, a, v) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4] + v[0]; out[5] = a[5] + v[1]; return out; } /** * 旋转变换 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} a * @param {number} rad */ function rotate(out, a, rad) { var aa = a[0]; var ac = a[2]; var atx = a[4]; var ab = a[1]; var ad = a[3]; var aty = a[5]; var st = Math.sin(rad); var ct = Math.cos(rad); out[0] = aa * ct + ab * st; out[1] = -aa * st + ab * ct; out[2] = ac * ct + ad * st; out[3] = -ac * st + ct * ad; out[4] = ct * atx + st * aty; out[5] = ct * aty - st * atx; return out; } /** * 缩放变换 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} a * @param {Float32Array|Array.} v */ function scale$1(out, a, v) { var vx = v[0]; var vy = v[1]; out[0] = a[0] * vx; out[1] = a[1] * vy; out[2] = a[2] * vx; out[3] = a[3] * vy; out[4] = a[4] * vx; out[5] = a[5] * vy; return out; } /** * 求逆矩阵 * @param {Float32Array|Array.} out * @param {Float32Array|Array.} a */ function invert(out, a) { var aa = a[0]; var ac = a[2]; var atx = a[4]; var ab = a[1]; var ad = a[3]; var aty = a[5]; var det = aa * ad - ab * ac; if (!det) { return null; } det = 1.0 / det; out[0] = ad * det; out[1] = -ab * det; out[2] = -ac * det; out[3] = aa * det; out[4] = (ac * aty - ad * atx) * det; out[5] = (ab * atx - aa * aty) * det; return out; } /** * Clone a new matrix. * @param {Float32Array|Array.} a */ function clone$2(a) { var b = create$1(); copy$1(b, a); return b; } var matrix = (Object.freeze || Object)({ create: create$1, identity: identity, copy: copy$1, mul: mul$1, translate: translate, rotate: rotate, scale: scale$1, invert: invert, clone: clone$2 }); /** * 提供变换扩展 * @module zrender/mixin/Transformable * @author pissang (https://www.github.com/pissang) */ var mIdentity = identity; var EPSILON = 5e-5; function isNotAroundZero(val) { return val > EPSILON || val < -EPSILON; } /** * @alias module:zrender/mixin/Transformable * @constructor */ var Transformable = function (opts) { opts = opts || {}; // If there are no given position, rotation, scale if (!opts.position) { /** * 平移 * @type {Array.} * @default [0, 0] */ this.position = [0, 0]; } if (opts.rotation == null) { /** * 旋转 * @type {Array.} * @default 0 */ this.rotation = 0; } if (!opts.scale) { /** * 缩放 * @type {Array.} * @default [1, 1] */ this.scale = [1, 1]; } /** * 旋转和缩放的原点 * @type {Array.} * @default null */ this.origin = this.origin || null; }; var transformableProto = Transformable.prototype; transformableProto.transform = null; /** * 判断是否需要有坐标变换 * 如果有坐标变换, 则从position, rotation, scale以及父节点的transform计算出自身的transform矩阵 */ transformableProto.needLocalTransform = function () { return isNotAroundZero(this.rotation) || isNotAroundZero(this.position[0]) || isNotAroundZero(this.position[1]) || isNotAroundZero(this.scale[0] - 1) || isNotAroundZero(this.scale[1] - 1); }; var scaleTmp = []; transformableProto.updateTransform = function () { var parent = this.parent; var parentHasTransform = parent && parent.transform; var needLocalTransform = this.needLocalTransform(); var m = this.transform; if (!(needLocalTransform || parentHasTransform)) { m && mIdentity(m); return; } m = m || create$1(); if (needLocalTransform) { this.getLocalTransform(m); } else { mIdentity(m); } // 应用父节点变换 if (parentHasTransform) { if (needLocalTransform) { mul$1(m, parent.transform, m); } else { copy$1(m, parent.transform); } } // 保存这个变换矩阵 this.transform = m; var globalScaleRatio = this.globalScaleRatio; if (globalScaleRatio != null && globalScaleRatio !== 1) { this.getGlobalScale(scaleTmp); var relX = scaleTmp[0] < 0 ? -1 : 1; var relY = scaleTmp[1] < 0 ? -1 : 1; var sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0; var sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0; m[0] *= sx; m[1] *= sx; m[2] *= sy; m[3] *= sy; } this.invTransform = this.invTransform || create$1(); invert(this.invTransform, m); }; transformableProto.getLocalTransform = function (m) { return Transformable.getLocalTransform(this, m); }; /** * 将自己的transform应用到context上 * @param {CanvasRenderingContext2D} ctx */ transformableProto.setTransform = function (ctx) { var m = this.transform; var dpr = ctx.dpr || 1; if (m) { ctx.setTransform(dpr * m[0], dpr * m[1], dpr * m[2], dpr * m[3], dpr * m[4], dpr * m[5]); } else { ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } }; transformableProto.restoreTransform = function (ctx) { var dpr = ctx.dpr || 1; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; var tmpTransform = []; var originTransform = create$1(); transformableProto.setLocalTransform = function (m) { if (!m) { // TODO return or set identity? return; } var sx = m[0] * m[0] + m[1] * m[1]; var sy = m[2] * m[2] + m[3] * m[3]; var position = this.position; var scale$$1 = this.scale; if (isNotAroundZero(sx - 1)) { sx = Math.sqrt(sx); } if (isNotAroundZero(sy - 1)) { sy = Math.sqrt(sy); } if (m[0] < 0) { sx = -sx; } if (m[3] < 0) { sy = -sy; } position[0] = m[4]; position[1] = m[5]; scale$$1[0] = sx; scale$$1[1] = sy; this.rotation = Math.atan2(-m[1] / sy, m[0] / sx); }; /** * 分解`transform`矩阵到`position`, `rotation`, `scale` */ transformableProto.decomposeTransform = function () { if (!this.transform) { return; } var parent = this.parent; var m = this.transform; if (parent && parent.transform) { // Get local transform and decompose them to position, scale, rotation mul$1(tmpTransform, parent.invTransform, m); m = tmpTransform; } var origin = this.origin; if (origin && (origin[0] || origin[1])) { originTransform[4] = origin[0]; originTransform[5] = origin[1]; mul$1(tmpTransform, m, originTransform); tmpTransform[4] -= origin[0]; tmpTransform[5] -= origin[1]; m = tmpTransform; } this.setLocalTransform(m); }; /** * Get global scale * @return {Array.} */ transformableProto.getGlobalScale = function (out) { var m = this.transform; out = out || []; if (!m) { out[0] = 1; out[1] = 1; return out; } out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]); out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]); if (m[0] < 0) { out[0] = -out[0]; } if (m[3] < 0) { out[1] = -out[1]; } return out; }; /** * 变换坐标位置到 shape 的局部坐标空间 * @method * @param {number} x * @param {number} y * @return {Array.} */ transformableProto.transformCoordToLocal = function (x, y) { var v2 = [x, y]; var invTransform = this.invTransform; if (invTransform) { applyTransform(v2, v2, invTransform); } return v2; }; /** * 变换局部坐标位置到全局坐标空间 * @method * @param {number} x * @param {number} y * @return {Array.} */ transformableProto.transformCoordToGlobal = function (x, y) { var v2 = [x, y]; var transform = this.transform; if (transform) { applyTransform(v2, v2, transform); } return v2; }; /** * @static * @param {Object} target * @param {Array.} target.origin * @param {number} target.rotation * @param {Array.} target.position * @param {Array.} [m] */ Transformable.getLocalTransform = function (target, m) { m = m || []; mIdentity(m); var origin = target.origin; var scale$$1 = target.scale || [1, 1]; var rotation = target.rotation || 0; var position = target.position || [0, 0]; if (origin) { // Translate to origin m[4] -= origin[0]; m[5] -= origin[1]; } scale$1(m, m, scale$$1); if (rotation) { rotate(m, m, rotation); } if (origin) { // Translate back from origin m[4] += origin[0]; m[5] += origin[1]; } m[4] += position[0]; m[5] += position[1]; return m; }; /** * 缓动代码来自 https://github.com/sole/tween.js/blob/master/src/Tween.js * @see http://sole.github.io/tween.js/examples/03_graphs.html * @exports zrender/animation/easing */ var easing = { /** * @param {number} k * @return {number} */ linear: function (k) { return k; }, /** * @param {number} k * @return {number} */ quadraticIn: function (k) { return k * k; }, /** * @param {number} k * @return {number} */ quadraticOut: function (k) { return k * (2 - k); }, /** * @param {number} k * @return {number} */ quadraticInOut: function (k) { if ((k *= 2) < 1) { return 0.5 * k * k; } return -0.5 * (--k * (k - 2) - 1); }, // 三次方的缓动(t^3) /** * @param {number} k * @return {number} */ cubicIn: function (k) { return k * k * k; }, /** * @param {number} k * @return {number} */ cubicOut: function (k) { return --k * k * k + 1; }, /** * @param {number} k * @return {number} */ cubicInOut: function (k) { if ((k *= 2) < 1) { return 0.5 * k * k * k; } return 0.5 * ((k -= 2) * k * k + 2); }, // 四次方的缓动(t^4) /** * @param {number} k * @return {number} */ quarticIn: function (k) { return k * k * k * k; }, /** * @param {number} k * @return {number} */ quarticOut: function (k) { return 1 - (--k * k * k * k); }, /** * @param {number} k * @return {number} */ quarticInOut: function (k) { if ((k *= 2) < 1) { return 0.5 * k * k * k * k; } return -0.5 * ((k -= 2) * k * k * k - 2); }, // 五次方的缓动(t^5) /** * @param {number} k * @return {number} */ quinticIn: function (k) { return k * k * k * k * k; }, /** * @param {number} k * @return {number} */ quinticOut: function (k) { return --k * k * k * k * k + 1; }, /** * @param {number} k * @return {number} */ quinticInOut: function (k) { if ((k *= 2) < 1) { return 0.5 * k * k * k * k * k; } return 0.5 * ((k -= 2) * k * k * k * k + 2); }, // 正弦曲线的缓动(sin(t)) /** * @param {number} k * @return {number} */ sinusoidalIn: function (k) { return 1 - Math.cos(k * Math.PI / 2); }, /** * @param {number} k * @return {number} */ sinusoidalOut: function (k) { return Math.sin(k * Math.PI / 2); }, /** * @param {number} k * @return {number} */ sinusoidalInOut: function (k) { return 0.5 * (1 - Math.cos(Math.PI * k)); }, // 指数曲线的缓动(2^t) /** * @param {number} k * @return {number} */ exponentialIn: function (k) { return k === 0 ? 0 : Math.pow(1024, k - 1); }, /** * @param {number} k * @return {number} */ exponentialOut: function (k) { return k === 1 ? 1 : 1 - Math.pow(2, -10 * k); }, /** * @param {number} k * @return {number} */ exponentialInOut: function (k) { if (k === 0) { return 0; } if (k === 1) { return 1; } if ((k *= 2) < 1) { return 0.5 * Math.pow(1024, k - 1); } return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2); }, // 圆形曲线的缓动(sqrt(1-t^2)) /** * @param {number} k * @return {number} */ circularIn: function (k) { return 1 - Math.sqrt(1 - k * k); }, /** * @param {number} k * @return {number} */ circularOut: function (k) { return Math.sqrt(1 - (--k * k)); }, /** * @param {number} k * @return {number} */ circularInOut: function (k) { if ((k *= 2) < 1) { return -0.5 * (Math.sqrt(1 - k * k) - 1); } return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); }, // 创建类似于弹簧在停止前来回振荡的动画 /** * @param {number} k * @return {number} */ elasticIn: function (k) { var s; var a = 0.1; var p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; } else { s = p * Math.asin(1 / a) / (2 * Math.PI); } return -(a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); }, /** * @param {number} k * @return {number} */ elasticOut: function (k) { var s; var a = 0.1; var p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; } else { s = p * Math.asin(1 / a) / (2 * Math.PI); } return (a * Math.pow(2, -10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); }, /** * @param {number} k * @return {number} */ elasticInOut: function (k) { var s; var a = 0.1; var p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; } else { s = p * Math.asin(1 / a) / (2 * Math.PI); } if ((k *= 2) < 1) { return -0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); } return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; }, // 在某一动画开始沿指示的路径进行动画处理前稍稍收回该动画的移动 /** * @param {number} k * @return {number} */ backIn: function (k) { var s = 1.70158; return k * k * ((s + 1) * k - s); }, /** * @param {number} k * @return {number} */ backOut: function (k) { var s = 1.70158; return --k * k * ((s + 1) * k + s) + 1; }, /** * @param {number} k * @return {number} */ backInOut: function (k) { var s = 1.70158 * 1.525; if ((k *= 2) < 1) { return 0.5 * (k * k * ((s + 1) * k - s)); } return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); }, // 创建弹跳效果 /** * @param {number} k * @return {number} */ bounceIn: function (k) { return 1 - easing.bounceOut(1 - k); }, /** * @param {number} k * @return {number} */ bounceOut: function (k) { if (k < (1 / 2.75)) { return 7.5625 * k * k; } else if (k < (2 / 2.75)) { return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; } else if (k < (2.5 / 2.75)) { return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; } else { return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; } }, /** * @param {number} k * @return {number} */ bounceInOut: function (k) { if (k < 0.5) { return easing.bounceIn(k * 2) * 0.5; } return easing.bounceOut(k * 2 - 1) * 0.5 + 0.5; } }; /** * 动画主控制器 * @config target 动画对象,可以是数组,如果是数组的话会批量分发onframe等事件 * @config life(1000) 动画时长 * @config delay(0) 动画延迟时间 * @config loop(true) * @config gap(0) 循环的间隔时间 * @config onframe * @config easing(optional) * @config ondestroy(optional) * @config onrestart(optional) * * TODO pause */ function Clip(options) { this._target = options.target; // 生命周期 this._life = options.life || 1000; // 延时 this._delay = options.delay || 0; // 开始时间 // this._startTime = new Date().getTime() + this._delay;// 单位毫秒 this._initialized = false; // 是否循环 this.loop = options.loop == null ? false : options.loop; this.gap = options.gap || 0; this.easing = options.easing || 'Linear'; this.onframe = options.onframe; this.ondestroy = options.ondestroy; this.onrestart = options.onrestart; this._pausedTime = 0; this._paused = false; } Clip.prototype = { constructor: Clip, step: function (globalTime, deltaTime) { // Set startTime on first step, or _startTime may has milleseconds different between clips // PENDING if (!this._initialized) { this._startTime = globalTime + this._delay; this._initialized = true; } if (this._paused) { this._pausedTime += deltaTime; return; } var percent = (globalTime - this._startTime - this._pausedTime) / this._life; // 还没开始 if (percent < 0) { return; } percent = Math.min(percent, 1); var easing$$1 = this.easing; var easingFunc = typeof easing$$1 === 'string' ? easing[easing$$1] : easing$$1; var schedule = typeof easingFunc === 'function' ? easingFunc(percent) : percent; this.fire('frame', schedule); // 结束 if (percent === 1) { if (this.loop) { this.restart(globalTime); // 重新开始周期 // 抛出而不是直接调用事件直到 stage.update 后再统一调用这些事件 return 'restart'; } // 动画完成将这个控制器标识为待删除 // 在Animation.update中进行批量删除 this._needsRemove = true; return 'destroy'; } return null; }, restart: function (globalTime) { var remainder = (globalTime - this._startTime - this._pausedTime) % this._life; this._startTime = globalTime - remainder + this.gap; this._pausedTime = 0; this._needsRemove = false; }, fire: function (eventType, arg) { eventType = 'on' + eventType; if (this[eventType]) { this[eventType](this._target, arg); } }, pause: function () { this._paused = true; }, resume: function () { this._paused = false; } }; // Simple LRU cache use doubly linked list // @module zrender/core/LRU /** * Simple double linked list. Compared with array, it has O(1) remove operation. * @constructor */ var LinkedList = function () { /** * @type {module:zrender/core/LRU~Entry} */ this.head = null; /** * @type {module:zrender/core/LRU~Entry} */ this.tail = null; this._len = 0; }; var linkedListProto = LinkedList.prototype; /** * Insert a new value at the tail * @param {} val * @return {module:zrender/core/LRU~Entry} */ linkedListProto.insert = function (val) { var entry = new Entry(val); this.insertEntry(entry); return entry; }; /** * Insert an entry at the tail * @param {module:zrender/core/LRU~Entry} entry */ linkedListProto.insertEntry = function (entry) { if (!this.head) { this.head = this.tail = entry; } else { this.tail.next = entry; entry.prev = this.tail; entry.next = null; this.tail = entry; } this._len++; }; /** * Remove entry. * @param {module:zrender/core/LRU~Entry} entry */ linkedListProto.remove = function (entry) { var prev = entry.prev; var next = entry.next; if (prev) { prev.next = next; } else { // Is head this.head = next; } if (next) { next.prev = prev; } else { // Is tail this.tail = prev; } entry.next = entry.prev = null; this._len--; }; /** * @return {number} */ linkedListProto.len = function () { return this._len; }; /** * Clear list */ linkedListProto.clear = function () { this.head = this.tail = null; this._len = 0; }; /** * @constructor * @param {} val */ var Entry = function (val) { /** * @type {} */ this.value = val; /** * @type {module:zrender/core/LRU~Entry} */ this.next; /** * @type {module:zrender/core/LRU~Entry} */ this.prev; }; /** * LRU Cache * @constructor * @alias module:zrender/core/LRU */ var LRU = function (maxSize) { this._list = new LinkedList(); this._map = {}; this._maxSize = maxSize || 10; this._lastRemovedEntry = null; }; var LRUProto = LRU.prototype; /** * @param {string} key * @param {} value * @return {} Removed value */ LRUProto.put = function (key, value) { var list = this._list; var map = this._map; var removed = null; if (map[key] == null) { var len = list.len(); // Reuse last removed entry var entry = this._lastRemovedEntry; if (len >= this._maxSize && len > 0) { // Remove the least recently used var leastUsedEntry = list.head; list.remove(leastUsedEntry); delete map[leastUsedEntry.key]; removed = leastUsedEntry.value; this._lastRemovedEntry = leastUsedEntry; } if (entry) { entry.value = value; } else { entry = new Entry(value); } entry.key = key; list.insertEntry(entry); map[key] = entry; } return removed; }; /** * @param {string} key * @return {} */ LRUProto.get = function (key) { var entry = this._map[key]; var list = this._list; if (entry != null) { // Put the latest used entry in the tail if (entry !== list.tail) { list.remove(entry); list.insertEntry(entry); } return entry.value; } }; /** * Clear the cache */ LRUProto.clear = function () { this._list.clear(); this._map = {}; }; var kCSSColorTable = { 'transparent': [0, 0, 0, 0], 'aliceblue': [240, 248, 255, 1], 'antiquewhite': [250, 235, 215, 1], 'aqua': [0, 255, 255, 1], 'aquamarine': [127, 255, 212, 1], 'azure': [240, 255, 255, 1], 'beige': [245, 245, 220, 1], 'bisque': [255, 228, 196, 1], 'black': [0, 0, 0, 1], 'blanchedalmond': [255, 235, 205, 1], 'blue': [0, 0, 255, 1], 'blueviolet': [138, 43, 226, 1], 'brown': [165, 42, 42, 1], 'burlywood': [222, 184, 135, 1], 'cadetblue': [95, 158, 160, 1], 'chartreuse': [127, 255, 0, 1], 'chocolate': [210, 105, 30, 1], 'coral': [255, 127, 80, 1], 'cornflowerblue': [100, 149, 237, 1], 'cornsilk': [255, 248, 220, 1], 'crimson': [220, 20, 60, 1], 'cyan': [0, 255, 255, 1], 'darkblue': [0, 0, 139, 1], 'darkcyan': [0, 139, 139, 1], 'darkgoldenrod': [184, 134, 11, 1], 'darkgray': [169, 169, 169, 1], 'darkgreen': [0, 100, 0, 1], 'darkgrey': [169, 169, 169, 1], 'darkkhaki': [189, 183, 107, 1], 'darkmagenta': [139, 0, 139, 1], 'darkolivegreen': [85, 107, 47, 1], 'darkorange': [255, 140, 0, 1], 'darkorchid': [153, 50, 204, 1], 'darkred': [139, 0, 0, 1], 'darksalmon': [233, 150, 122, 1], 'darkseagreen': [143, 188, 143, 1], 'darkslateblue': [72, 61, 139, 1], 'darkslategray': [47, 79, 79, 1], 'darkslategrey': [47, 79, 79, 1], 'darkturquoise': [0, 206, 209, 1], 'darkviolet': [148, 0, 211, 1], 'deeppink': [255, 20, 147, 1], 'deepskyblue': [0, 191, 255, 1], 'dimgray': [105, 105, 105, 1], 'dimgrey': [105, 105, 105, 1], 'dodgerblue': [30, 144, 255, 1], 'firebrick': [178, 34, 34, 1], 'floralwhite': [255, 250, 240, 1], 'forestgreen': [34, 139, 34, 1], 'fuchsia': [255, 0, 255, 1], 'gainsboro': [220, 220, 220, 1], 'ghostwhite': [248, 248, 255, 1], 'gold': [255, 215, 0, 1], 'goldenrod': [218, 165, 32, 1], 'gray': [128, 128, 128, 1], 'green': [0, 128, 0, 1], 'greenyellow': [173, 255, 47, 1], 'grey': [128, 128, 128, 1], 'honeydew': [240, 255, 240, 1], 'hotpink': [255, 105, 180, 1], 'indianred': [205, 92, 92, 1], 'indigo': [75, 0, 130, 1], 'ivory': [255, 255, 240, 1], 'khaki': [240, 230, 140, 1], 'lavender': [230, 230, 250, 1], 'lavenderblush': [255, 240, 245, 1], 'lawngreen': [124, 252, 0, 1], 'lemonchiffon': [255, 250, 205, 1], 'lightblue': [173, 216, 230, 1], 'lightcoral': [240, 128, 128, 1], 'lightcyan': [224, 255, 255, 1], 'lightgoldenrodyellow': [250, 250, 210, 1], 'lightgray': [211, 211, 211, 1], 'lightgreen': [144, 238, 144, 1], 'lightgrey': [211, 211, 211, 1], 'lightpink': [255, 182, 193, 1], 'lightsalmon': [255, 160, 122, 1], 'lightseagreen': [32, 178, 170, 1], 'lightskyblue': [135, 206, 250, 1], 'lightslategray': [119, 136, 153, 1], 'lightslategrey': [119, 136, 153, 1], 'lightsteelblue': [176, 196, 222, 1], 'lightyellow': [255, 255, 224, 1], 'lime': [0, 255, 0, 1], 'limegreen': [50, 205, 50, 1], 'linen': [250, 240, 230, 1], 'magenta': [255, 0, 255, 1], 'maroon': [128, 0, 0, 1], 'mediumaquamarine': [102, 205, 170, 1], 'mediumblue': [0, 0, 205, 1], 'mediumorchid': [186, 85, 211, 1], 'mediumpurple': [147, 112, 219, 1], 'mediumseagreen': [60, 179, 113, 1], 'mediumslateblue': [123, 104, 238, 1], 'mediumspringgreen': [0, 250, 154, 1], 'mediumturquoise': [72, 209, 204, 1], 'mediumvioletred': [199, 21, 133, 1], 'midnightblue': [25, 25, 112, 1], 'mintcream': [245, 255, 250, 1], 'mistyrose': [255, 228, 225, 1], 'moccasin': [255, 228, 181, 1], 'navajowhite': [255, 222, 173, 1], 'navy': [0, 0, 128, 1], 'oldlace': [253, 245, 230, 1], 'olive': [128, 128, 0, 1], 'olivedrab': [107, 142, 35, 1], 'orange': [255, 165, 0, 1], 'orangered': [255, 69, 0, 1], 'orchid': [218, 112, 214, 1], 'palegoldenrod': [238, 232, 170, 1], 'palegreen': [152, 251, 152, 1], 'paleturquoise': [175, 238, 238, 1], 'palevioletred': [219, 112, 147, 1], 'papayawhip': [255, 239, 213, 1], 'peachpuff': [255, 218, 185, 1], 'peru': [205, 133, 63, 1], 'pink': [255, 192, 203, 1], 'plum': [221, 160, 221, 1], 'powderblue': [176, 224, 230, 1], 'purple': [128, 0, 128, 1], 'red': [255, 0, 0, 1], 'rosybrown': [188, 143, 143, 1], 'royalblue': [65, 105, 225, 1], 'saddlebrown': [139, 69, 19, 1], 'salmon': [250, 128, 114, 1], 'sandybrown': [244, 164, 96, 1], 'seagreen': [46, 139, 87, 1], 'seashell': [255, 245, 238, 1], 'sienna': [160, 82, 45, 1], 'silver': [192, 192, 192, 1], 'skyblue': [135, 206, 235, 1], 'slateblue': [106, 90, 205, 1], 'slategray': [112, 128, 144, 1], 'slategrey': [112, 128, 144, 1], 'snow': [255, 250, 250, 1], 'springgreen': [0, 255, 127, 1], 'steelblue': [70, 130, 180, 1], 'tan': [210, 180, 140, 1], 'teal': [0, 128, 128, 1], 'thistle': [216, 191, 216, 1], 'tomato': [255, 99, 71, 1], 'turquoise': [64, 224, 208, 1], 'violet': [238, 130, 238, 1], 'wheat': [245, 222, 179, 1], 'white': [255, 255, 255, 1], 'whitesmoke': [245, 245, 245, 1], 'yellow': [255, 255, 0, 1], 'yellowgreen': [154, 205, 50, 1] }; function clampCssByte(i) { // Clamp to integer 0 .. 255. i = Math.round(i); // Seems to be what Chrome does (vs truncation). return i < 0 ? 0 : i > 255 ? 255 : i; } function clampCssAngle(i) { // Clamp to integer 0 .. 360. i = Math.round(i); // Seems to be what Chrome does (vs truncation). return i < 0 ? 0 : i > 360 ? 360 : i; } function clampCssFloat(f) { // Clamp to float 0.0 .. 1.0. return f < 0 ? 0 : f > 1 ? 1 : f; } function parseCssInt(str) { // int or percentage. if (str.length && str.charAt(str.length - 1) === '%') { return clampCssByte(parseFloat(str) / 100 * 255); } return clampCssByte(parseInt(str, 10)); } function parseCssFloat(str) { // float or percentage. if (str.length && str.charAt(str.length - 1) === '%') { return clampCssFloat(parseFloat(str) / 100); } return clampCssFloat(parseFloat(str)); } function cssHueToRgb(m1, m2, h) { if (h < 0) { h += 1; } else if (h > 1) { h -= 1; } if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } if (h * 2 < 1) { return m2; } if (h * 3 < 2) { return m1 + (m2 - m1) * (2 / 3 - h) * 6; } return m1; } function lerpNumber(a, b, p) { return a + (b - a) * p; } function setRgba(out, r, g, b, a) { out[0] = r; out[1] = g; out[2] = b; out[3] = a; return out; } function copyRgba(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; } var colorCache = new LRU(20); var lastRemovedArr = null; function putToCache(colorStr, rgbaArr) { // Reuse removed array if (lastRemovedArr) { copyRgba(lastRemovedArr, rgbaArr); } lastRemovedArr = colorCache.put(colorStr, lastRemovedArr || (rgbaArr.slice())); } /** * @param {string} colorStr * @param {Array.} out * @return {Array.} * @memberOf module:zrender/util/color */ function parse(colorStr, rgbaArr) { if (!colorStr) { return; } rgbaArr = rgbaArr || []; var cached = colorCache.get(colorStr); if (cached) { return copyRgba(rgbaArr, cached); } // colorStr may be not string colorStr = colorStr + ''; // Remove all whitespace, not compliant, but should just be more accepting. var str = colorStr.replace(/ /g, '').toLowerCase(); // Color keywords (and transparent) lookup. if (str in kCSSColorTable) { copyRgba(rgbaArr, kCSSColorTable[str]); putToCache(colorStr, rgbaArr); return rgbaArr; } // #abc and #abc123 syntax. if (str.charAt(0) === '#') { if (str.length === 4) { var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. if (!(iv >= 0 && iv <= 0xfff)) { setRgba(rgbaArr, 0, 0, 0, 1); return; // Covers NaN. } setRgba(rgbaArr, ((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8), (iv & 0xf0) | ((iv & 0xf0) >> 4), (iv & 0xf) | ((iv & 0xf) << 4), 1 ); putToCache(colorStr, rgbaArr); return rgbaArr; } else if (str.length === 7) { var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. if (!(iv >= 0 && iv <= 0xffffff)) { setRgba(rgbaArr, 0, 0, 0, 1); return; // Covers NaN. } setRgba(rgbaArr, (iv & 0xff0000) >> 16, (iv & 0xff00) >> 8, iv & 0xff, 1 ); putToCache(colorStr, rgbaArr); return rgbaArr; } return; } var op = str.indexOf('('); var ep = str.indexOf(')'); if (op !== -1 && ep + 1 === str.length) { var fname = str.substr(0, op); var params = str.substr(op + 1, ep - (op + 1)).split(','); var alpha = 1; // To allow case fallthrough. switch (fname) { case 'rgba': if (params.length !== 4) { setRgba(rgbaArr, 0, 0, 0, 1); return; } alpha = parseCssFloat(params.pop()); // jshint ignore:line // Fall through. case 'rgb': if (params.length !== 3) { setRgba(rgbaArr, 0, 0, 0, 1); return; } setRgba(rgbaArr, parseCssInt(params[0]), parseCssInt(params[1]), parseCssInt(params[2]), alpha ); putToCache(colorStr, rgbaArr); return rgbaArr; case 'hsla': if (params.length !== 4) { setRgba(rgbaArr, 0, 0, 0, 1); return; } params[3] = parseCssFloat(params[3]); hsla2rgba(params, rgbaArr); putToCache(colorStr, rgbaArr); return rgbaArr; case 'hsl': if (params.length !== 3) { setRgba(rgbaArr, 0, 0, 0, 1); return; } hsla2rgba(params, rgbaArr); putToCache(colorStr, rgbaArr); return rgbaArr; default: return; } } setRgba(rgbaArr, 0, 0, 0, 1); return; } /** * @param {Array.} hsla * @param {Array.} rgba * @return {Array.} rgba */ function hsla2rgba(hsla, rgba) { var h = (((parseFloat(hsla[0]) % 360) + 360) % 360) / 360; // 0 .. 1 // NOTE(deanm): According to the CSS spec s/l should only be // percentages, but we don't bother and let float or percentage. var s = parseCssFloat(hsla[1]); var l = parseCssFloat(hsla[2]); var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; var m1 = l * 2 - m2; rgba = rgba || []; setRgba(rgba, clampCssByte(cssHueToRgb(m1, m2, h + 1 / 3) * 255), clampCssByte(cssHueToRgb(m1, m2, h) * 255), clampCssByte(cssHueToRgb(m1, m2, h - 1 / 3) * 255), 1 ); if (hsla.length === 4) { rgba[3] = hsla[3]; } return rgba; } /** * @param {Array.} rgba * @return {Array.} hsla */ function rgba2hsla(rgba) { if (!rgba) { return; } // RGB from 0 to 255 var R = rgba[0] / 255; var G = rgba[1] / 255; var B = rgba[2] / 255; var vMin = Math.min(R, G, B); // Min. value of RGB var vMax = Math.max(R, G, B); // Max. value of RGB var delta = vMax - vMin; // Delta RGB value var L = (vMax + vMin) / 2; var H; var S; // HSL results from 0 to 1 if (delta === 0) { H = 0; S = 0; } else { if (L < 0.5) { S = delta / (vMax + vMin); } else { S = delta / (2 - vMax - vMin); } var deltaR = (((vMax - R) / 6) + (delta / 2)) / delta; var deltaG = (((vMax - G) / 6) + (delta / 2)) / delta; var deltaB = (((vMax - B) / 6) + (delta / 2)) / delta; if (R === vMax) { H = deltaB - deltaG; } else if (G === vMax) { H = (1 / 3) + deltaR - deltaB; } else if (B === vMax) { H = (2 / 3) + deltaG - deltaR; } if (H < 0) { H += 1; } if (H > 1) { H -= 1; } } var hsla = [H * 360, S, L]; if (rgba[3] != null) { hsla.push(rgba[3]); } return hsla; } /** * @param {string} color * @param {number} level * @return {string} * @memberOf module:zrender/util/color */ function lift(color, level) { var colorArr = parse(color); if (colorArr) { for (var i = 0; i < 3; i++) { if (level < 0) { colorArr[i] = colorArr[i] * (1 - level) | 0; } else { colorArr[i] = ((255 - colorArr[i]) * level + colorArr[i]) | 0; } if (colorArr[i] > 255) { colorArr[i] = 255; } else if (color[i] < 0) { colorArr[i] = 0; } } return stringify(colorArr, colorArr.length === 4 ? 'rgba' : 'rgb'); } } /** * @param {string} color * @return {string} * @memberOf module:zrender/util/color */ function toHex(color) { var colorArr = parse(color); if (colorArr) { return ((1 << 24) + (colorArr[0] << 16) + (colorArr[1] << 8) + (+colorArr[2])).toString(16).slice(1); } } /** * Map value to color. Faster than lerp methods because color is represented by rgba array. * @param {number} normalizedValue A float between 0 and 1. * @param {Array.>} colors List of rgba color array * @param {Array.} [out] Mapped gba color array * @return {Array.} will be null/undefined if input illegal. */ function fastLerp(normalizedValue, colors, out) { if (!(colors && colors.length) || !(normalizedValue >= 0 && normalizedValue <= 1) ) { return; } out = out || []; var value = normalizedValue * (colors.length - 1); var leftIndex = Math.floor(value); var rightIndex = Math.ceil(value); var leftColor = colors[leftIndex]; var rightColor = colors[rightIndex]; var dv = value - leftIndex; out[0] = clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv)); out[1] = clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv)); out[2] = clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv)); out[3] = clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv)); return out; } /** * @deprecated */ var fastMapToColor = fastLerp; /** * @param {number} normalizedValue A float between 0 and 1. * @param {Array.} colors Color list. * @param {boolean=} fullOutput Default false. * @return {(string|Object)} Result color. If fullOutput, * return {color: ..., leftIndex: ..., rightIndex: ..., value: ...}, * @memberOf module:zrender/util/color */ function lerp$1(normalizedValue, colors, fullOutput) { if (!(colors && colors.length) || !(normalizedValue >= 0 && normalizedValue <= 1) ) { return; } var value = normalizedValue * (colors.length - 1); var leftIndex = Math.floor(value); var rightIndex = Math.ceil(value); var leftColor = parse(colors[leftIndex]); var rightColor = parse(colors[rightIndex]); var dv = value - leftIndex; var color = stringify( [ clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv)), clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv)), clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv)), clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv)) ], 'rgba' ); return fullOutput ? { color: color, leftIndex: leftIndex, rightIndex: rightIndex, value: value } : color; } /** * @deprecated */ var mapToColor = lerp$1; /** * @param {string} color * @param {number=} h 0 ~ 360, ignore when null. * @param {number=} s 0 ~ 1, ignore when null. * @param {number=} l 0 ~ 1, ignore when null. * @return {string} Color string in rgba format. * @memberOf module:zrender/util/color */ function modifyHSL(color, h, s, l) { color = parse(color); if (color) { color = rgba2hsla(color); h != null && (color[0] = clampCssAngle(h)); s != null && (color[1] = parseCssFloat(s)); l != null && (color[2] = parseCssFloat(l)); return stringify(hsla2rgba(color), 'rgba'); } } /** * @param {string} color * @param {number=} alpha 0 ~ 1 * @return {string} Color string in rgba format. * @memberOf module:zrender/util/color */ function modifyAlpha(color, alpha) { color = parse(color); if (color && alpha != null) { color[3] = clampCssFloat(alpha); return stringify(color, 'rgba'); } } /** * @param {Array.} arrColor like [12,33,44,0.4] * @param {string} type 'rgba', 'hsva', ... * @return {string} Result color. (If input illegal, return undefined). */ function stringify(arrColor, type) { if (!arrColor || !arrColor.length) { return; } var colorStr = arrColor[0] + ',' + arrColor[1] + ',' + arrColor[2]; if (type === 'rgba' || type === 'hsva' || type === 'hsla') { colorStr += ',' + arrColor[3]; } return type + '(' + colorStr + ')'; } var color = (Object.freeze || Object)({ parse: parse, lift: lift, toHex: toHex, fastLerp: fastLerp, fastMapToColor: fastMapToColor, lerp: lerp$1, mapToColor: mapToColor, modifyHSL: modifyHSL, modifyAlpha: modifyAlpha, stringify: stringify }); /** * @module echarts/animation/Animator */ var arraySlice = Array.prototype.slice; function defaultGetter(target, key) { return target[key]; } function defaultSetter(target, key, value) { target[key] = value; } /** * @param {number} p0 * @param {number} p1 * @param {number} percent * @return {number} */ function interpolateNumber(p0, p1, percent) { return (p1 - p0) * percent + p0; } /** * @param {string} p0 * @param {string} p1 * @param {number} percent * @return {string} */ function interpolateString(p0, p1, percent) { return percent > 0.5 ? p1 : p0; } /** * @param {Array} p0 * @param {Array} p1 * @param {number} percent * @param {Array} out * @param {number} arrDim */ function interpolateArray(p0, p1, percent, out, arrDim) { var len = p0.length; if (arrDim === 1) { for (var i = 0; i < len; i++) { out[i] = interpolateNumber(p0[i], p1[i], percent); } } else { var len2 = len && p0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { out[i][j] = interpolateNumber( p0[i][j], p1[i][j], percent ); } } } } // arr0 is source array, arr1 is target array. // Do some preprocess to avoid error happened when interpolating from arr0 to arr1 function fillArr(arr0, arr1, arrDim) { var arr0Len = arr0.length; var arr1Len = arr1.length; if (arr0Len !== arr1Len) { // FIXME Not work for TypedArray var isPreviousLarger = arr0Len > arr1Len; if (isPreviousLarger) { // Cut the previous arr0.length = arr1Len; } else { // Fill the previous for (var i = arr0Len; i < arr1Len; i++) { arr0.push( arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i]) ); } } } // Handling NaN value var len2 = arr0[0] && arr0[0].length; for (var i = 0; i < arr0.length; i++) { if (arrDim === 1) { if (isNaN(arr0[i])) { arr0[i] = arr1[i]; } } else { for (var j = 0; j < len2; j++) { if (isNaN(arr0[i][j])) { arr0[i][j] = arr1[i][j]; } } } } } /** * @param {Array} arr0 * @param {Array} arr1 * @param {number} arrDim * @return {boolean} */ function isArraySame(arr0, arr1, arrDim) { if (arr0 === arr1) { return true; } var len = arr0.length; if (len !== arr1.length) { return false; } if (arrDim === 1) { for (var i = 0; i < len; i++) { if (arr0[i] !== arr1[i]) { return false; } } } else { var len2 = arr0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { if (arr0[i][j] !== arr1[i][j]) { return false; } } } } return true; } /** * Catmull Rom interpolate array * @param {Array} p0 * @param {Array} p1 * @param {Array} p2 * @param {Array} p3 * @param {number} t * @param {number} t2 * @param {number} t3 * @param {Array} out * @param {number} arrDim */ function catmullRomInterpolateArray( p0, p1, p2, p3, t, t2, t3, out, arrDim ) { var len = p0.length; if (arrDim === 1) { for (var i = 0; i < len; i++) { out[i] = catmullRomInterpolate( p0[i], p1[i], p2[i], p3[i], t, t2, t3 ); } } else { var len2 = p0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { out[i][j] = catmullRomInterpolate( p0[i][j], p1[i][j], p2[i][j], p3[i][j], t, t2, t3 ); } } } } /** * Catmull Rom interpolate number * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {number} t * @param {number} t2 * @param {number} t3 * @return {number} */ function catmullRomInterpolate(p0, p1, p2, p3, t, t2, t3) { var v0 = (p2 - p0) * 0.5; var v1 = (p3 - p1) * 0.5; return (2 * (p1 - p2) + v0 + v1) * t3 + (-3 * (p1 - p2) - 2 * v0 - v1) * t2 + v0 * t + p1; } function cloneValue(value) { if (isArrayLike(value)) { var len = value.length; if (isArrayLike(value[0])) { var ret = []; for (var i = 0; i < len; i++) { ret.push(arraySlice.call(value[i])); } return ret; } return arraySlice.call(value); } return value; } function rgba2String(rgba) { rgba[0] = Math.floor(rgba[0]); rgba[1] = Math.floor(rgba[1]); rgba[2] = Math.floor(rgba[2]); return 'rgba(' + rgba.join(',') + ')'; } function getArrayDim(keyframes) { var lastValue = keyframes[keyframes.length - 1].value; return isArrayLike(lastValue && lastValue[0]) ? 2 : 1; } function createTrackClip(animator, easing, oneTrackDone, keyframes, propName, forceAnimate) { var getter = animator._getter; var setter = animator._setter; var useSpline = easing === 'spline'; var trackLen = keyframes.length; if (!trackLen) { return; } // Guess data type var firstVal = keyframes[0].value; var isValueArray = isArrayLike(firstVal); var isValueColor = false; var isValueString = false; // For vertices morphing var arrDim = isValueArray ? getArrayDim(keyframes) : 0; var trackMaxTime; // Sort keyframe as ascending keyframes.sort(function (a, b) { return a.time - b.time; }); trackMaxTime = keyframes[trackLen - 1].time; // Percents of each keyframe var kfPercents = []; // Value of each keyframe var kfValues = []; var prevValue = keyframes[0].value; var isAllValueEqual = true; for (var i = 0; i < trackLen; i++) { kfPercents.push(keyframes[i].time / trackMaxTime); // Assume value is a color when it is a string var value = keyframes[i].value; // Check if value is equal, deep check if value is array if (!((isValueArray && isArraySame(value, prevValue, arrDim)) || (!isValueArray && value === prevValue))) { isAllValueEqual = false; } prevValue = value; // Try converting a string to a color array if (typeof value === 'string') { var colorArray = parse(value); if (colorArray) { value = colorArray; isValueColor = true; } else { isValueString = true; } } kfValues.push(value); } if (!forceAnimate && isAllValueEqual) { return; } var lastValue = kfValues[trackLen - 1]; // Polyfill array and NaN value for (var i = 0; i < trackLen - 1; i++) { if (isValueArray) { fillArr(kfValues[i], lastValue, arrDim); } else { if (isNaN(kfValues[i]) && !isNaN(lastValue) && !isValueString && !isValueColor) { kfValues[i] = lastValue; } } } isValueArray && fillArr(getter(animator._target, propName), lastValue, arrDim); // Cache the key of last frame to speed up when // animation playback is sequency var lastFrame = 0; var lastFramePercent = 0; var start; var w; var p0; var p1; var p2; var p3; if (isValueColor) { var rgba = [0, 0, 0, 0]; } var onframe = function (target, percent) { // Find the range keyframes // kf1-----kf2---------current--------kf3 // find kf2 and kf3 and do interpolation var frame; // In the easing function like elasticOut, percent may less than 0 if (percent < 0) { frame = 0; } else if (percent < lastFramePercent) { // Start from next key // PENDING start from lastFrame ? start = Math.min(lastFrame + 1, trackLen - 1); for (frame = start; frame >= 0; frame--) { if (kfPercents[frame] <= percent) { break; } } // PENDING really need to do this ? frame = Math.min(frame, trackLen - 2); } else { for (frame = lastFrame; frame < trackLen; frame++) { if (kfPercents[frame] > percent) { break; } } frame = Math.min(frame - 1, trackLen - 2); } lastFrame = frame; lastFramePercent = percent; var range = (kfPercents[frame + 1] - kfPercents[frame]); if (range === 0) { return; } else { w = (percent - kfPercents[frame]) / range; } if (useSpline) { p1 = kfValues[frame]; p0 = kfValues[frame === 0 ? frame : frame - 1]; p2 = kfValues[frame > trackLen - 2 ? trackLen - 1 : frame + 1]; p3 = kfValues[frame > trackLen - 3 ? trackLen - 1 : frame + 2]; if (isValueArray) { catmullRomInterpolateArray( p0, p1, p2, p3, w, w * w, w * w * w, getter(target, propName), arrDim ); } else { var value; if (isValueColor) { value = catmullRomInterpolateArray( p0, p1, p2, p3, w, w * w, w * w * w, rgba, 1 ); value = rgba2String(rgba); } else if (isValueString) { // String is step(0.5) return interpolateString(p1, p2, w); } else { value = catmullRomInterpolate( p0, p1, p2, p3, w, w * w, w * w * w ); } setter( target, propName, value ); } } else { if (isValueArray) { interpolateArray( kfValues[frame], kfValues[frame + 1], w, getter(target, propName), arrDim ); } else { var value; if (isValueColor) { interpolateArray( kfValues[frame], kfValues[frame + 1], w, rgba, 1 ); value = rgba2String(rgba); } else if (isValueString) { // String is step(0.5) return interpolateString(kfValues[frame], kfValues[frame + 1], w); } else { value = interpolateNumber(kfValues[frame], kfValues[frame + 1], w); } setter( target, propName, value ); } } }; var clip = new Clip({ target: animator._target, life: trackMaxTime, loop: animator._loop, delay: animator._delay, onframe: onframe, ondestroy: oneTrackDone }); if (easing && easing !== 'spline') { clip.easing = easing; } return clip; } /** * @alias module:zrender/animation/Animator * @constructor * @param {Object} target * @param {boolean} loop * @param {Function} getter * @param {Function} setter */ var Animator = function (target, loop, getter, setter) { this._tracks = {}; this._target = target; this._loop = loop || false; this._getter = getter || defaultGetter; this._setter = setter || defaultSetter; this._clipCount = 0; this._delay = 0; this._doneList = []; this._onframeList = []; this._clipList = []; }; Animator.prototype = { /** * Set Animation keyframe * @param {number} time 关键帧时间,单位是ms * @param {Object} props 关键帧的属性值,key-value表示 * @return {module:zrender/animation/Animator} */ when: function (time /* ms */, props) { var tracks = this._tracks; for (var propName in props) { if (!props.hasOwnProperty(propName)) { continue; } if (!tracks[propName]) { tracks[propName] = []; // Invalid value var value = this._getter(this._target, propName); if (value == null) { // zrLog('Invalid property ' + propName); continue; } // If time is 0 // Then props is given initialize value // Else // Initialize value from current prop value if (time !== 0) { tracks[propName].push({ time: 0, value: cloneValue(value) }); } } tracks[propName].push({ time: time, value: props[propName] }); } return this; }, /** * 添加动画每一帧的回调函数 * @param {Function} callback * @return {module:zrender/animation/Animator} */ during: function (callback) { this._onframeList.push(callback); return this; }, pause: function () { for (var i = 0; i < this._clipList.length; i++) { this._clipList[i].pause(); } this._paused = true; }, resume: function () { for (var i = 0; i < this._clipList.length; i++) { this._clipList[i].resume(); } this._paused = false; }, isPaused: function () { return !!this._paused; }, _doneCallback: function () { // Clear all tracks this._tracks = {}; // Clear all clips this._clipList.length = 0; var doneList = this._doneList; var len = doneList.length; for (var i = 0; i < len; i++) { doneList[i].call(this); } }, /** * Start the animation * @param {string|Function} [easing] * 动画缓动函数,详见{@link module:zrender/animation/easing} * @param {boolean} forceAnimate * @return {module:zrender/animation/Animator} */ start: function (easing, forceAnimate) { var self = this; var clipCount = 0; var oneTrackDone = function () { clipCount--; if (!clipCount) { self._doneCallback(); } }; var lastClip; for (var propName in this._tracks) { if (!this._tracks.hasOwnProperty(propName)) { continue; } var clip = createTrackClip( this, easing, oneTrackDone, this._tracks[propName], propName, forceAnimate ); if (clip) { this._clipList.push(clip); clipCount++; // If start after added to animation if (this.animation) { this.animation.addClip(clip); } lastClip = clip; } } // Add during callback on the last clip if (lastClip) { var oldOnFrame = lastClip.onframe; lastClip.onframe = function (target, percent) { oldOnFrame(target, percent); for (var i = 0; i < self._onframeList.length; i++) { self._onframeList[i](target, percent); } }; } // This optimization will help the case that in the upper application // the view may be refreshed frequently, where animation will be // called repeatly but nothing changed. if (!clipCount) { this._doneCallback(); } return this; }, /** * Stop animation * @param {boolean} forwardToLast If move to last frame before stop */ stop: function (forwardToLast) { var clipList = this._clipList; var animation = this.animation; for (var i = 0; i < clipList.length; i++) { var clip = clipList[i]; if (forwardToLast) { // Move to last frame before stop clip.onframe(this._target, 1); } animation && animation.removeClip(clip); } clipList.length = 0; }, /** * Set when animation delay starts * @param {number} time 单位ms * @return {module:zrender/animation/Animator} */ delay: function (time) { this._delay = time; return this; }, /** * Add callback for animation end * @param {Function} cb * @return {module:zrender/animation/Animator} */ done: function (cb) { if (cb) { this._doneList.push(cb); } return this; }, /** * @return {Array.} */ getClips: function () { return this._clipList; } }; var dpr = 1; // If in browser environment if (typeof window !== 'undefined') { dpr = Math.max(window.devicePixelRatio || 1, 1); } /** * config默认配置项 * @exports zrender/config * @author Kener (@Kener-林峰, kener.linfeng@gmail.com) */ /** * Debug log mode: * 0: Do nothing, for release. * 1: console.error, for debug. */ var debugMode = 0; // retina 屏幕优化 var devicePixelRatio = dpr; var logError = function () { }; if (debugMode === 1) { logError = console.error; } var logError$1 = logError; /** * @alias module:zrender/mixin/Animatable * @constructor */ var Animatable = function () { /** * @type {Array.} * @readOnly */ this.animators = []; }; Animatable.prototype = { constructor: Animatable, /** * 动画 * * @param {string} path The path to fetch value from object, like 'a.b.c'. * @param {boolean} [loop] Whether to loop animation. * @return {module:zrender/animation/Animator} * @example: * el.animate('style', false) * .when(1000, {x: 10} ) * .done(function(){ // Animation done }) * .start() */ animate: function (path, loop) { var target; var animatingShape = false; var el = this; var zr = this.__zr; if (path) { var pathSplitted = path.split('.'); var prop = el; // If animating shape animatingShape = pathSplitted[0] === 'shape'; for (var i = 0, l = pathSplitted.length; i < l; i++) { if (!prop) { continue; } prop = prop[pathSplitted[i]]; } if (prop) { target = prop; } } else { target = el; } if (!target) { logError$1( 'Property "' + path + '" is not existed in element ' + el.id ); return; } var animators = el.animators; var animator = new Animator(target, loop); animator.during(function (target) { el.dirty(animatingShape); }) .done(function () { // FIXME Animator will not be removed if use `Animator#stop` to stop animation animators.splice(indexOf(animators, animator), 1); }); animators.push(animator); // If animate after added to the zrender if (zr) { zr.animation.addAnimator(animator); } return animator; }, /** * 停止动画 * @param {boolean} forwardToLast If move to last frame before stop */ stopAnimation: function (forwardToLast) { var animators = this.animators; var len = animators.length; for (var i = 0; i < len; i++) { animators[i].stop(forwardToLast); } animators.length = 0; return this; }, /** * Caution: this method will stop previous animation. * So do not use this method to one element twice before * animation starts, unless you know what you are doing. * @param {Object} target * @param {number} [time=500] Time in ms * @param {string} [easing='linear'] * @param {number} [delay=0] * @param {Function} [callback] * @param {Function} [forceAnimate] Prevent stop animation and callback * immediently when target values are the same as current values. * * @example * // Animate position * el.animateTo({ * position: [10, 10] * }, function () { // done }) * * // Animate shape, style and position in 100ms, delayed 100ms, with cubicOut easing * el.animateTo({ * shape: { * width: 500 * }, * style: { * fill: 'red' * } * position: [10, 10] * }, 100, 100, 'cubicOut', function () { // done }) */ // TODO Return animation key animateTo: function (target, time, delay, easing, callback, forceAnimate) { animateTo(this, target, time, delay, easing, callback, forceAnimate); }, /** * Animate from the target state to current state. * The params and the return value are the same as `this.animateTo`. */ animateFrom: function (target, time, delay, easing, callback, forceAnimate) { animateTo(this, target, time, delay, easing, callback, forceAnimate, true); } }; function animateTo(animatable, target, time, delay, easing, callback, forceAnimate, reverse) { // animateTo(target, time, easing, callback); if (isString(delay)) { callback = easing; easing = delay; delay = 0; } // animateTo(target, time, delay, callback); else if (isFunction$1(easing)) { callback = easing; easing = 'linear'; delay = 0; } // animateTo(target, time, callback); else if (isFunction$1(delay)) { callback = delay; delay = 0; } // animateTo(target, callback) else if (isFunction$1(time)) { callback = time; time = 500; } // animateTo(target) else if (!time) { time = 500; } // Stop all previous animations animatable.stopAnimation(); animateToShallow(animatable, '', animatable, target, time, delay, reverse); // Animators may be removed immediately after start // if there is nothing to animate var animators = animatable.animators.slice(); var count = animators.length; function done() { count--; if (!count) { callback && callback(); } } // No animators. This should be checked before animators[i].start(), // because 'done' may be executed immediately if no need to animate. if (!count) { callback && callback(); } // Start after all animators created // Incase any animator is done immediately when all animation properties are not changed for (var i = 0; i < animators.length; i++) { animators[i] .done(done) .start(easing, forceAnimate); } } /** * @param {string} path='' * @param {Object} source=animatable * @param {Object} target * @param {number} [time=500] * @param {number} [delay=0] * @param {boolean} [reverse] If `true`, animate * from the `target` to current state. * * @example * // Animate position * el._animateToShallow({ * position: [10, 10] * }) * * // Animate shape, style and position in 100ms, delayed 100ms * el._animateToShallow({ * shape: { * width: 500 * }, * style: { * fill: 'red' * } * position: [10, 10] * }, 100, 100) */ function animateToShallow(animatable, path, source, target, time, delay, reverse) { var objShallow = {}; var propertyCount = 0; for (var name in target) { if (!target.hasOwnProperty(name)) { continue; } if (source[name] != null) { if (isObject$1(target[name]) && !isArrayLike(target[name])) { animateToShallow( animatable, path ? path + '.' + name : name, source[name], target[name], time, delay, reverse ); } else { if (reverse) { objShallow[name] = source[name]; setAttrByPath(animatable, path, name, target[name]); } else { objShallow[name] = target[name]; } propertyCount++; } } else if (target[name] != null && !reverse) { setAttrByPath(animatable, path, name, target[name]); } } if (propertyCount > 0) { animatable.animate(path, false) .when(time == null ? 500 : time, objShallow) .delay(delay || 0); } } function setAttrByPath(el, path, name, value) { // Attr directly if not has property // FIXME, if some property not needed for element ? if (!path) { el.attr(name, value); } else { // Only support set shape or style var props = {}; props[path] = {}; props[path][name] = value; el.attr(props); } } /** * @alias module:zrender/Element * @constructor * @extends {module:zrender/mixin/Animatable} * @extends {module:zrender/mixin/Transformable} * @extends {module:zrender/mixin/Eventful} */ var Element = function (opts) { // jshint ignore:line Transformable.call(this, opts); Eventful.call(this, opts); Animatable.call(this, opts); /** * 画布元素ID * @type {string} */ this.id = opts.id || guid(); }; Element.prototype = { /** * 元素类型 * Element type * @type {string} */ type: 'element', /** * 元素名字 * Element name * @type {string} */ name: '', /** * ZRender 实例对象,会在 element 添加到 zrender 实例中后自动赋值 * ZRender instance will be assigned when element is associated with zrender * @name module:/zrender/Element#__zr * @type {module:zrender/ZRender} */ __zr: null, /** * 图形是否忽略,为true时忽略图形的绘制以及事件触发 * If ignore drawing and events of the element object * @name module:/zrender/Element#ignore * @type {boolean} * @default false */ ignore: false, /** * 用于裁剪的路径(shape),所有 Group 内的路径在绘制时都会被这个路径裁剪 * 该路径会继承被裁减对象的变换 * @type {module:zrender/graphic/Path} * @see http://www.w3.org/TR/2dcontext/#clipping-region * @readOnly */ clipPath: null, /** * 是否是 Group * @type {boolean} */ isGroup: false, /** * Drift element * @param {number} dx dx on the global space * @param {number} dy dy on the global space */ drift: function (dx, dy) { switch (this.draggable) { case 'horizontal': dy = 0; break; case 'vertical': dx = 0; break; } var m = this.transform; if (!m) { m = this.transform = [1, 0, 0, 1, 0, 0]; } m[4] += dx; m[5] += dy; this.decomposeTransform(); this.dirty(false); }, /** * Hook before update */ beforeUpdate: function () {}, /** * Hook after update */ afterUpdate: function () {}, /** * Update each frame */ update: function () { this.updateTransform(); }, /** * @param {Function} cb * @param {} context */ traverse: function (cb, context) {}, /** * @protected */ attrKV: function (key, value) { if (key === 'position' || key === 'scale' || key === 'origin') { // Copy the array if (value) { var target = this[key]; if (!target) { target = this[key] = []; } target[0] = value[0]; target[1] = value[1]; } } else { this[key] = value; } }, /** * Hide the element */ hide: function () { this.ignore = true; this.__zr && this.__zr.refresh(); }, /** * Show the element */ show: function () { this.ignore = false; this.__zr && this.__zr.refresh(); }, /** * @param {string|Object} key * @param {*} value */ attr: function (key, value) { if (typeof key === 'string') { this.attrKV(key, value); } else if (isObject$1(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { this.attrKV(name, key[name]); } } } this.dirty(false); return this; }, /** * @param {module:zrender/graphic/Path} clipPath */ setClipPath: function (clipPath) { var zr = this.__zr; if (zr) { clipPath.addSelfToZr(zr); } // Remove previous clip path if (this.clipPath && this.clipPath !== clipPath) { this.removeClipPath(); } this.clipPath = clipPath; clipPath.__zr = zr; clipPath.__clipTarget = this; this.dirty(false); }, /** */ removeClipPath: function () { var clipPath = this.clipPath; if (clipPath) { if (clipPath.__zr) { clipPath.removeSelfFromZr(clipPath.__zr); } clipPath.__zr = null; clipPath.__clipTarget = null; this.clipPath = null; this.dirty(false); } }, /** * Add self from zrender instance. * Not recursively because it will be invoked when element added to storage. * @param {module:zrender/ZRender} zr */ addSelfToZr: function (zr) { this.__zr = zr; // 添加动画 var animators = this.animators; if (animators) { for (var i = 0; i < animators.length; i++) { zr.animation.addAnimator(animators[i]); } } if (this.clipPath) { this.clipPath.addSelfToZr(zr); } }, /** * Remove self from zrender instance. * Not recursively because it will be invoked when element added to storage. * @param {module:zrender/ZRender} zr */ removeSelfFromZr: function (zr) { this.__zr = null; // 移除动画 var animators = this.animators; if (animators) { for (var i = 0; i < animators.length; i++) { zr.animation.removeAnimator(animators[i]); } } if (this.clipPath) { this.clipPath.removeSelfFromZr(zr); } } }; mixin(Element, Animatable); mixin(Element, Transformable); mixin(Element, Eventful); /** * @module echarts/core/BoundingRect */ var v2ApplyTransform = applyTransform; var mathMin = Math.min; var mathMax = Math.max; /** * @alias module:echarts/core/BoundingRect */ function BoundingRect(x, y, width, height) { if (width < 0) { x = x + width; width = -width; } if (height < 0) { y = y + height; height = -height; } /** * @type {number} */ this.x = x; /** * @type {number} */ this.y = y; /** * @type {number} */ this.width = width; /** * @type {number} */ this.height = height; } BoundingRect.prototype = { constructor: BoundingRect, /** * @param {module:echarts/core/BoundingRect} other */ union: function (other) { var x = mathMin(other.x, this.x); var y = mathMin(other.y, this.y); this.width = mathMax( other.x + other.width, this.x + this.width ) - x; this.height = mathMax( other.y + other.height, this.y + this.height ) - y; this.x = x; this.y = y; }, /** * @param {Array.} m * @methods */ applyTransform: (function () { var lt = []; var rb = []; var lb = []; var rt = []; return function (m) { // In case usage like this // el.getBoundingRect().applyTransform(el.transform) // And element has no transform if (!m) { return; } lt[0] = lb[0] = this.x; lt[1] = rt[1] = this.y; rb[0] = rt[0] = this.x + this.width; rb[1] = lb[1] = this.y + this.height; v2ApplyTransform(lt, lt, m); v2ApplyTransform(rb, rb, m); v2ApplyTransform(lb, lb, m); v2ApplyTransform(rt, rt, m); this.x = mathMin(lt[0], rb[0], lb[0], rt[0]); this.y = mathMin(lt[1], rb[1], lb[1], rt[1]); var maxX = mathMax(lt[0], rb[0], lb[0], rt[0]); var maxY = mathMax(lt[1], rb[1], lb[1], rt[1]); this.width = maxX - this.x; this.height = maxY - this.y; }; })(), /** * Calculate matrix of transforming from self to target rect * @param {module:zrender/core/BoundingRect} b * @return {Array.} */ calculateTransform: function (b) { var a = this; var sx = b.width / a.width; var sy = b.height / a.height; var m = create$1(); // 矩阵右乘 translate(m, m, [-a.x, -a.y]); scale$1(m, m, [sx, sy]); translate(m, m, [b.x, b.y]); return m; }, /** * @param {(module:echarts/core/BoundingRect|Object)} b * @return {boolean} */ intersect: function (b) { if (!b) { return false; } if (!(b instanceof BoundingRect)) { // Normalize negative width/height. b = BoundingRect.create(b); } var a = this; var ax0 = a.x; var ax1 = a.x + a.width; var ay0 = a.y; var ay1 = a.y + a.height; var bx0 = b.x; var bx1 = b.x + b.width; var by0 = b.y; var by1 = b.y + b.height; return !(ax1 < bx0 || bx1 < ax0 || ay1 < by0 || by1 < ay0); }, contain: function (x, y) { var rect = this; return x >= rect.x && x <= (rect.x + rect.width) && y >= rect.y && y <= (rect.y + rect.height); }, /** * @return {module:echarts/core/BoundingRect} */ clone: function () { return new BoundingRect(this.x, this.y, this.width, this.height); }, /** * Copy from another rect */ copy: function (other) { this.x = other.x; this.y = other.y; this.width = other.width; this.height = other.height; }, plain: function () { return { x: this.x, y: this.y, width: this.width, height: this.height }; } }; /** * @param {Object|module:zrender/core/BoundingRect} rect * @param {number} rect.x * @param {number} rect.y * @param {number} rect.width * @param {number} rect.height * @return {module:zrender/core/BoundingRect} */ BoundingRect.create = function (rect) { return new BoundingRect(rect.x, rect.y, rect.width, rect.height); }; /** * Group是一个容器,可以插入子节点,Group的变换也会被应用到子节点上 * @module zrender/graphic/Group * @example * var Group = require('zrender/container/Group'); * var Circle = require('zrender/graphic/shape/Circle'); * var g = new Group(); * g.position[0] = 100; * g.position[1] = 100; * g.add(new Circle({ * style: { * x: 100, * y: 100, * r: 20, * } * })); * zr.add(g); */ /** * @alias module:zrender/graphic/Group * @constructor * @extends module:zrender/mixin/Transformable * @extends module:zrender/mixin/Eventful */ var Group = function (opts) { opts = opts || {}; Element.call(this, opts); for (var key in opts) { if (opts.hasOwnProperty(key)) { this[key] = opts[key]; } } this._children = []; this.__storage = null; this.__dirty = true; }; Group.prototype = { constructor: Group, isGroup: true, /** * @type {string} */ type: 'group', /** * 所有子孙元素是否响应鼠标事件 * @name module:/zrender/container/Group#silent * @type {boolean} * @default false */ silent: false, /** * @return {Array.} */ children: function () { return this._children.slice(); }, /** * 获取指定 index 的儿子节点 * @param {number} idx * @return {module:zrender/Element} */ childAt: function (idx) { return this._children[idx]; }, /** * 获取指定名字的儿子节点 * @param {string} name * @return {module:zrender/Element} */ childOfName: function (name) { var children = this._children; for (var i = 0; i < children.length; i++) { if (children[i].name === name) { return children[i]; } } }, /** * @return {number} */ childCount: function () { return this._children.length; }, /** * 添加子节点到最后 * @param {module:zrender/Element} child */ add: function (child) { if (child && child !== this && child.parent !== this) { this._children.push(child); this._doAdd(child); } return this; }, /** * 添加子节点在 nextSibling 之前 * @param {module:zrender/Element} child * @param {module:zrender/Element} nextSibling */ addBefore: function (child, nextSibling) { if (child && child !== this && child.parent !== this && nextSibling && nextSibling.parent === this) { var children = this._children; var idx = children.indexOf(nextSibling); if (idx >= 0) { children.splice(idx, 0, child); this._doAdd(child); } } return this; }, _doAdd: function (child) { if (child.parent) { child.parent.remove(child); } child.parent = this; var storage = this.__storage; var zr = this.__zr; if (storage && storage !== child.__storage) { storage.addToStorage(child); if (child instanceof Group) { child.addChildrenToStorage(storage); } } zr && zr.refresh(); }, /** * 移除子节点 * @param {module:zrender/Element} child */ remove: function (child) { var zr = this.__zr; var storage = this.__storage; var children = this._children; var idx = indexOf(children, child); if (idx < 0) { return this; } children.splice(idx, 1); child.parent = null; if (storage) { storage.delFromStorage(child); if (child instanceof Group) { child.delChildrenFromStorage(storage); } } zr && zr.refresh(); return this; }, /** * 移除所有子节点 */ removeAll: function () { var children = this._children; var storage = this.__storage; var child; var i; for (i = 0; i < children.length; i++) { child = children[i]; if (storage) { storage.delFromStorage(child); if (child instanceof Group) { child.delChildrenFromStorage(storage); } } child.parent = null; } children.length = 0; return this; }, /** * 遍历所有子节点 * @param {Function} cb * @param {} context */ eachChild: function (cb, context) { var children = this._children; for (var i = 0; i < children.length; i++) { var child = children[i]; cb.call(context, child, i); } return this; }, /** * 深度优先遍历所有子孙节点 * @param {Function} cb * @param {} context */ traverse: function (cb, context) { for (var i = 0; i < this._children.length; i++) { var child = this._children[i]; cb.call(context, child); if (child.type === 'group') { child.traverse(cb, context); } } return this; }, addChildrenToStorage: function (storage) { for (var i = 0; i < this._children.length; i++) { var child = this._children[i]; storage.addToStorage(child); if (child instanceof Group) { child.addChildrenToStorage(storage); } } }, delChildrenFromStorage: function (storage) { for (var i = 0; i < this._children.length; i++) { var child = this._children[i]; storage.delFromStorage(child); if (child instanceof Group) { child.delChildrenFromStorage(storage); } } }, dirty: function () { this.__dirty = true; this.__zr && this.__zr.refresh(); return this; }, /** * @return {module:zrender/core/BoundingRect} */ getBoundingRect: function (includeChildren) { // TODO Caching var rect = null; var tmpRect = new BoundingRect(0, 0, 0, 0); var children = includeChildren || this._children; var tmpMat = []; for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.ignore || child.invisible) { continue; } var childRect = child.getBoundingRect(); var transform = child.getLocalTransform(tmpMat); // TODO // The boundingRect cacluated by transforming original // rect may be bigger than the actual bundingRect when rotation // is used. (Consider a circle rotated aginst its center, where // the actual boundingRect should be the same as that not be // rotated.) But we can not find better approach to calculate // actual boundingRect yet, considering performance. if (transform) { tmpRect.copy(childRect); tmpRect.applyTransform(transform); rect = rect || tmpRect.clone(); rect.union(tmpRect); } else { rect = rect || childRect.clone(); rect.union(childRect); } } return rect || tmpRect; } }; inherits(Group, Element); // https://github.com/mziccard/node-timsort var DEFAULT_MIN_MERGE = 32; var DEFAULT_MIN_GALLOPING = 7; function minRunLength(n) { var r = 0; while (n >= DEFAULT_MIN_MERGE) { r |= n & 1; n >>= 1; } return n + r; } function makeAscendingRun(array, lo, hi, compare) { var runHi = lo + 1; if (runHi === hi) { return 1; } if (compare(array[runHi++], array[lo]) < 0) { while (runHi < hi && compare(array[runHi], array[runHi - 1]) < 0) { runHi++; } reverseRun(array, lo, runHi); } else { while (runHi < hi && compare(array[runHi], array[runHi - 1]) >= 0) { runHi++; } } return runHi - lo; } function reverseRun(array, lo, hi) { hi--; while (lo < hi) { var t = array[lo]; array[lo++] = array[hi]; array[hi--] = t; } } function binaryInsertionSort(array, lo, hi, start, compare) { if (start === lo) { start++; } for (; start < hi; start++) { var pivot = array[start]; var left = lo; var right = start; var mid; while (left < right) { mid = left + right >>> 1; if (compare(pivot, array[mid]) < 0) { right = mid; } else { left = mid + 1; } } var n = start - left; switch (n) { case 3: array[left + 3] = array[left + 2]; case 2: array[left + 2] = array[left + 1]; case 1: array[left + 1] = array[left]; break; default: while (n > 0) { array[left + n] = array[left + n - 1]; n--; } } array[left] = pivot; } } function gallopLeft(value, array, start, length, hint, compare) { var lastOffset = 0; var maxOffset = 0; var offset = 1; if (compare(value, array[start + hint]) > 0) { maxOffset = length - hint; while (offset < maxOffset && compare(value, array[start + hint + offset]) > 0) { lastOffset = offset; offset = (offset << 1) + 1; if (offset <= 0) { offset = maxOffset; } } if (offset > maxOffset) { offset = maxOffset; } lastOffset += hint; offset += hint; } else { maxOffset = hint + 1; while (offset < maxOffset && compare(value, array[start + hint - offset]) <= 0) { lastOffset = offset; offset = (offset << 1) + 1; if (offset <= 0) { offset = maxOffset; } } if (offset > maxOffset) { offset = maxOffset; } var tmp = lastOffset; lastOffset = hint - offset; offset = hint - tmp; } lastOffset++; while (lastOffset < offset) { var m = lastOffset + (offset - lastOffset >>> 1); if (compare(value, array[start + m]) > 0) { lastOffset = m + 1; } else { offset = m; } } return offset; } function gallopRight(value, array, start, length, hint, compare) { var lastOffset = 0; var maxOffset = 0; var offset = 1; if (compare(value, array[start + hint]) < 0) { maxOffset = hint + 1; while (offset < maxOffset && compare(value, array[start + hint - offset]) < 0) { lastOffset = offset; offset = (offset << 1) + 1; if (offset <= 0) { offset = maxOffset; } } if (offset > maxOffset) { offset = maxOffset; } var tmp = lastOffset; lastOffset = hint - offset; offset = hint - tmp; } else { maxOffset = length - hint; while (offset < maxOffset && compare(value, array[start + hint + offset]) >= 0) { lastOffset = offset; offset = (offset << 1) + 1; if (offset <= 0) { offset = maxOffset; } } if (offset > maxOffset) { offset = maxOffset; } lastOffset += hint; offset += hint; } lastOffset++; while (lastOffset < offset) { var m = lastOffset + (offset - lastOffset >>> 1); if (compare(value, array[start + m]) < 0) { offset = m; } else { lastOffset = m + 1; } } return offset; } function TimSort(array, compare) { var minGallop = DEFAULT_MIN_GALLOPING; var runStart; var runLength; var stackSize = 0; var tmp = []; runStart = []; runLength = []; function pushRun(_runStart, _runLength) { runStart[stackSize] = _runStart; runLength[stackSize] = _runLength; stackSize += 1; } function mergeRuns() { while (stackSize > 1) { var n = stackSize - 2; if ( (n >= 1 && runLength[n - 1] <= runLength[n] + runLength[n + 1]) || (n >= 2 && runLength[n - 2] <= runLength[n] + runLength[n - 1]) ) { if (runLength[n - 1] < runLength[n + 1]) { n--; } } else if (runLength[n] > runLength[n + 1]) { break; } mergeAt(n); } } function forceMergeRuns() { while (stackSize > 1) { var n = stackSize - 2; if (n > 0 && runLength[n - 1] < runLength[n + 1]) { n--; } mergeAt(n); } } function mergeAt(i) { var start1 = runStart[i]; var length1 = runLength[i]; var start2 = runStart[i + 1]; var length2 = runLength[i + 1]; runLength[i] = length1 + length2; if (i === stackSize - 3) { runStart[i + 1] = runStart[i + 2]; runLength[i + 1] = runLength[i + 2]; } stackSize--; var k = gallopRight(array[start2], array, start1, length1, 0, compare); start1 += k; length1 -= k; if (length1 === 0) { return; } length2 = gallopLeft(array[start1 + length1 - 1], array, start2, length2, length2 - 1, compare); if (length2 === 0) { return; } if (length1 <= length2) { mergeLow(start1, length1, start2, length2); } else { mergeHigh(start1, length1, start2, length2); } } function mergeLow(start1, length1, start2, length2) { var i = 0; for (i = 0; i < length1; i++) { tmp[i] = array[start1 + i]; } var cursor1 = 0; var cursor2 = start2; var dest = start1; array[dest++] = array[cursor2++]; if (--length2 === 0) { for (i = 0; i < length1; i++) { array[dest + i] = tmp[cursor1 + i]; } return; } if (length1 === 1) { for (i = 0; i < length2; i++) { array[dest + i] = array[cursor2 + i]; } array[dest + length2] = tmp[cursor1]; return; } var _minGallop = minGallop; var count1; var count2; var exit; while (1) { count1 = 0; count2 = 0; exit = false; do { if (compare(array[cursor2], tmp[cursor1]) < 0) { array[dest++] = array[cursor2++]; count2++; count1 = 0; if (--length2 === 0) { exit = true; break; } } else { array[dest++] = tmp[cursor1++]; count1++; count2 = 0; if (--length1 === 1) { exit = true; break; } } } while ((count1 | count2) < _minGallop); if (exit) { break; } do { count1 = gallopRight(array[cursor2], tmp, cursor1, length1, 0, compare); if (count1 !== 0) { for (i = 0; i < count1; i++) { array[dest + i] = tmp[cursor1 + i]; } dest += count1; cursor1 += count1; length1 -= count1; if (length1 <= 1) { exit = true; break; } } array[dest++] = array[cursor2++]; if (--length2 === 0) { exit = true; break; } count2 = gallopLeft(tmp[cursor1], array, cursor2, length2, 0, compare); if (count2 !== 0) { for (i = 0; i < count2; i++) { array[dest + i] = array[cursor2 + i]; } dest += count2; cursor2 += count2; length2 -= count2; if (length2 === 0) { exit = true; break; } } array[dest++] = tmp[cursor1++]; if (--length1 === 1) { exit = true; break; } _minGallop--; } while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING); if (exit) { break; } if (_minGallop < 0) { _minGallop = 0; } _minGallop += 2; } minGallop = _minGallop; minGallop < 1 && (minGallop = 1); if (length1 === 1) { for (i = 0; i < length2; i++) { array[dest + i] = array[cursor2 + i]; } array[dest + length2] = tmp[cursor1]; } else if (length1 === 0) { throw new Error(); // throw new Error('mergeLow preconditions were not respected'); } else { for (i = 0; i < length1; i++) { array[dest + i] = tmp[cursor1 + i]; } } } function mergeHigh(start1, length1, start2, length2) { var i = 0; for (i = 0; i < length2; i++) { tmp[i] = array[start2 + i]; } var cursor1 = start1 + length1 - 1; var cursor2 = length2 - 1; var dest = start2 + length2 - 1; var customCursor = 0; var customDest = 0; array[dest--] = array[cursor1--]; if (--length1 === 0) { customCursor = dest - (length2 - 1); for (i = 0; i < length2; i++) { array[customCursor + i] = tmp[i]; } return; } if (length2 === 1) { dest -= length1; cursor1 -= length1; customDest = dest + 1; customCursor = cursor1 + 1; for (i = length1 - 1; i >= 0; i--) { array[customDest + i] = array[customCursor + i]; } array[dest] = tmp[cursor2]; return; } var _minGallop = minGallop; while (true) { var count1 = 0; var count2 = 0; var exit = false; do { if (compare(tmp[cursor2], array[cursor1]) < 0) { array[dest--] = array[cursor1--]; count1++; count2 = 0; if (--length1 === 0) { exit = true; break; } } else { array[dest--] = tmp[cursor2--]; count2++; count1 = 0; if (--length2 === 1) { exit = true; break; } } } while ((count1 | count2) < _minGallop); if (exit) { break; } do { count1 = length1 - gallopRight(tmp[cursor2], array, start1, length1, length1 - 1, compare); if (count1 !== 0) { dest -= count1; cursor1 -= count1; length1 -= count1; customDest = dest + 1; customCursor = cursor1 + 1; for (i = count1 - 1; i >= 0; i--) { array[customDest + i] = array[customCursor + i]; } if (length1 === 0) { exit = true; break; } } array[dest--] = tmp[cursor2--]; if (--length2 === 1) { exit = true; break; } count2 = length2 - gallopLeft(array[cursor1], tmp, 0, length2, length2 - 1, compare); if (count2 !== 0) { dest -= count2; cursor2 -= count2; length2 -= count2; customDest = dest + 1; customCursor = cursor2 + 1; for (i = 0; i < count2; i++) { array[customDest + i] = tmp[customCursor + i]; } if (length2 <= 1) { exit = true; break; } } array[dest--] = array[cursor1--]; if (--length1 === 0) { exit = true; break; } _minGallop--; } while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING); if (exit) { break; } if (_minGallop < 0) { _minGallop = 0; } _minGallop += 2; } minGallop = _minGallop; if (minGallop < 1) { minGallop = 1; } if (length2 === 1) { dest -= length1; cursor1 -= length1; customDest = dest + 1; customCursor = cursor1 + 1; for (i = length1 - 1; i >= 0; i--) { array[customDest + i] = array[customCursor + i]; } array[dest] = tmp[cursor2]; } else if (length2 === 0) { throw new Error(); // throw new Error('mergeHigh preconditions were not respected'); } else { customCursor = dest - (length2 - 1); for (i = 0; i < length2; i++) { array[customCursor + i] = tmp[i]; } } } this.mergeRuns = mergeRuns; this.forceMergeRuns = forceMergeRuns; this.pushRun = pushRun; } function sort(array, compare, lo, hi) { if (!lo) { lo = 0; } if (!hi) { hi = array.length; } var remaining = hi - lo; if (remaining < 2) { return; } var runLength = 0; if (remaining < DEFAULT_MIN_MERGE) { runLength = makeAscendingRun(array, lo, hi, compare); binaryInsertionSort(array, lo, hi, lo + runLength, compare); return; } var ts = new TimSort(array, compare); var minRun = minRunLength(remaining); do { runLength = makeAscendingRun(array, lo, hi, compare); if (runLength < minRun) { var force = remaining; if (force > minRun) { force = minRun; } binaryInsertionSort(array, lo, lo + force, lo + runLength, compare); runLength = force; } ts.pushRun(lo, runLength); ts.mergeRuns(); remaining -= runLength; lo += runLength; } while (remaining !== 0); ts.forceMergeRuns(); } // Use timsort because in most case elements are partially sorted // https://jsfiddle.net/pissang/jr4x7mdm/8/ function shapeCompareFunc(a, b) { if (a.zlevel === b.zlevel) { if (a.z === b.z) { // if (a.z2 === b.z2) { // // FIXME Slow has renderidx compare // // http://stackoverflow.com/questions/20883421/sorting-in-javascript-should-every-compare-function-have-a-return-0-statement // // https://github.com/v8/v8/blob/47cce544a31ed5577ffe2963f67acb4144ee0232/src/js/array.js#L1012 // return a.__renderidx - b.__renderidx; // } return a.z2 - b.z2; } return a.z - b.z; } return a.zlevel - b.zlevel; } /** * 内容仓库 (M) * @alias module:zrender/Storage * @constructor */ var Storage = function () { // jshint ignore:line this._roots = []; this._displayList = []; this._displayListLen = 0; }; Storage.prototype = { constructor: Storage, /** * @param {Function} cb * */ traverse: function (cb, context) { for (var i = 0; i < this._roots.length; i++) { this._roots[i].traverse(cb, context); } }, /** * 返回所有图形的绘制队列 * @param {boolean} [update=false] 是否在返回前更新该数组 * @param {boolean} [includeIgnore=false] 是否包含 ignore 的数组, 在 update 为 true 的时候有效 * * 详见{@link module:zrender/graphic/Displayable.prototype.updateDisplayList} * @return {Array.} */ getDisplayList: function (update, includeIgnore) { includeIgnore = includeIgnore || false; if (update) { this.updateDisplayList(includeIgnore); } return this._displayList; }, /** * 更新图形的绘制队列。 * 每次绘制前都会调用,该方法会先深度优先遍历整个树,更新所有Group和Shape的变换并且把所有可见的Shape保存到数组中, * 最后根据绘制的优先级(zlevel > z > 插入顺序)排序得到绘制队列 * @param {boolean} [includeIgnore=false] 是否包含 ignore 的数组 */ updateDisplayList: function (includeIgnore) { this._displayListLen = 0; var roots = this._roots; var displayList = this._displayList; for (var i = 0, len = roots.length; i < len; i++) { this._updateAndAddDisplayable(roots[i], null, includeIgnore); } displayList.length = this._displayListLen; env$1.canvasSupported && sort(displayList, shapeCompareFunc); }, _updateAndAddDisplayable: function (el, clipPaths, includeIgnore) { if (el.ignore && !includeIgnore) { return; } el.beforeUpdate(); if (el.__dirty) { el.update(); } el.afterUpdate(); var userSetClipPath = el.clipPath; if (userSetClipPath) { // FIXME 效率影响 if (clipPaths) { clipPaths = clipPaths.slice(); } else { clipPaths = []; } var currentClipPath = userSetClipPath; var parentClipPath = el; // Recursively add clip path while (currentClipPath) { // clipPath 的变换是基于使用这个 clipPath 的元素 currentClipPath.parent = parentClipPath; currentClipPath.updateTransform(); clipPaths.push(currentClipPath); parentClipPath = currentClipPath; currentClipPath = currentClipPath.clipPath; } } if (el.isGroup) { var children = el._children; for (var i = 0; i < children.length; i++) { var child = children[i]; // Force to mark as dirty if group is dirty // FIXME __dirtyPath ? if (el.__dirty) { child.__dirty = true; } this._updateAndAddDisplayable(child, clipPaths, includeIgnore); } // Mark group clean here el.__dirty = false; } else { el.__clipPaths = clipPaths; this._displayList[this._displayListLen++] = el; } }, /** * 添加图形(Shape)或者组(Group)到根节点 * @param {module:zrender/Element} el */ addRoot: function (el) { if (el.__storage === this) { return; } if (el instanceof Group) { el.addChildrenToStorage(this); } this.addToStorage(el); this._roots.push(el); }, /** * 删除指定的图形(Shape)或者组(Group) * @param {string|Array.} [el] 如果为空清空整个Storage */ delRoot: function (el) { if (el == null) { // 不指定el清空 for (var i = 0; i < this._roots.length; i++) { var root = this._roots[i]; if (root instanceof Group) { root.delChildrenFromStorage(this); } } this._roots = []; this._displayList = []; this._displayListLen = 0; return; } if (el instanceof Array) { for (var i = 0, l = el.length; i < l; i++) { this.delRoot(el[i]); } return; } var idx = indexOf(this._roots, el); if (idx >= 0) { this.delFromStorage(el); this._roots.splice(idx, 1); if (el instanceof Group) { el.delChildrenFromStorage(this); } } }, addToStorage: function (el) { if (el) { el.__storage = this; el.dirty(false); } return this; }, delFromStorage: function (el) { if (el) { el.__storage = null; } return this; }, /** * 清空并且释放Storage */ dispose: function () { this._renderList = this._roots = null; }, displayableSortFunc: shapeCompareFunc }; var SHADOW_PROPS = { 'shadowBlur': 1, 'shadowOffsetX': 1, 'shadowOffsetY': 1, 'textShadowBlur': 1, 'textShadowOffsetX': 1, 'textShadowOffsetY': 1, 'textBoxShadowBlur': 1, 'textBoxShadowOffsetX': 1, 'textBoxShadowOffsetY': 1 }; var fixShadow = function (ctx, propName, value) { if (SHADOW_PROPS.hasOwnProperty(propName)) { return value *= ctx.dpr; } return value; }; var ContextCachedBy = { NONE: 0, STYLE_BIND: 1, PLAIN_TEXT: 2 }; // Avoid confused with 0/false. var WILL_BE_RESTORED = 9; var STYLE_COMMON_PROPS = [ ['shadowBlur', 0], ['shadowOffsetX', 0], ['shadowOffsetY', 0], ['shadowColor', '#000'], ['lineCap', 'butt'], ['lineJoin', 'miter'], ['miterLimit', 10] ]; // var SHADOW_PROPS = STYLE_COMMON_PROPS.slice(0, 4); // var LINE_PROPS = STYLE_COMMON_PROPS.slice(4); var Style = function (opts) { this.extendFrom(opts, false); }; function createLinearGradient(ctx, obj, rect) { var x = obj.x == null ? 0 : obj.x; var x2 = obj.x2 == null ? 1 : obj.x2; var y = obj.y == null ? 0 : obj.y; var y2 = obj.y2 == null ? 0 : obj.y2; if (!obj.global) { x = x * rect.width + rect.x; x2 = x2 * rect.width + rect.x; y = y * rect.height + rect.y; y2 = y2 * rect.height + rect.y; } // Fix NaN when rect is Infinity x = isNaN(x) ? 0 : x; x2 = isNaN(x2) ? 1 : x2; y = isNaN(y) ? 0 : y; y2 = isNaN(y2) ? 0 : y2; var canvasGradient = ctx.createLinearGradient(x, y, x2, y2); return canvasGradient; } function createRadialGradient(ctx, obj, rect) { var width = rect.width; var height = rect.height; var min = Math.min(width, height); var x = obj.x == null ? 0.5 : obj.x; var y = obj.y == null ? 0.5 : obj.y; var r = obj.r == null ? 0.5 : obj.r; if (!obj.global) { x = x * width + rect.x; y = y * height + rect.y; r = r * min; } var canvasGradient = ctx.createRadialGradient(x, y, 0, x, y, r); return canvasGradient; } Style.prototype = { constructor: Style, /** * @type {string} */ fill: '#000', /** * @type {string} */ stroke: null, /** * @type {number} */ opacity: 1, /** * @type {number} */ fillOpacity: null, /** * @type {number} */ strokeOpacity: null, /** * `true` is not supported. * `false`/`null`/`undefined` are the same. * `false` is used to remove lineDash in some * case that `null`/`undefined` can not be set. * (e.g., emphasis.lineStyle in echarts) * @type {Array.|boolean} */ lineDash: null, /** * @type {number} */ lineDashOffset: 0, /** * @type {number} */ shadowBlur: 0, /** * @type {number} */ shadowOffsetX: 0, /** * @type {number} */ shadowOffsetY: 0, /** * @type {number} */ lineWidth: 1, /** * If stroke ignore scale * @type {Boolean} */ strokeNoScale: false, // Bounding rect text configuration // Not affected by element transform /** * @type {string} */ text: null, /** * If `fontSize` or `fontFamily` exists, `font` will be reset by * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`. * So do not visit it directly in upper application (like echarts), * but use `contain/text#makeFont` instead. * @type {string} */ font: null, /** * The same as font. Use font please. * @deprecated * @type {string} */ textFont: null, /** * It helps merging respectively, rather than parsing an entire font string. * @type {string} */ fontStyle: null, /** * It helps merging respectively, rather than parsing an entire font string. * @type {string} */ fontWeight: null, /** * It helps merging respectively, rather than parsing an entire font string. * Should be 12 but not '12px'. * @type {number} */ fontSize: null, /** * It helps merging respectively, rather than parsing an entire font string. * @type {string} */ fontFamily: null, /** * Reserved for special functinality, like 'hr'. * @type {string} */ textTag: null, /** * @type {string} */ textFill: '#000', /** * @type {string} */ textStroke: null, /** * @type {number} */ textWidth: null, /** * Only for textBackground. * @type {number} */ textHeight: null, /** * textStroke may be set as some color as a default * value in upper applicaion, where the default value * of textStrokeWidth should be 0 to make sure that * user can choose to do not use text stroke. * @type {number} */ textStrokeWidth: 0, /** * @type {number} */ textLineHeight: null, /** * 'inside', 'left', 'right', 'top', 'bottom' * [x, y] * Based on x, y of rect. * @type {string|Array.} * @default 'inside' */ textPosition: 'inside', /** * If not specified, use the boundingRect of a `displayable`. * @type {Object} */ textRect: null, /** * [x, y] * @type {Array.} */ textOffset: null, /** * @type {string} */ textAlign: null, /** * @type {string} */ textVerticalAlign: null, /** * @type {number} */ textDistance: 5, /** * @type {string} */ textShadowColor: 'transparent', /** * @type {number} */ textShadowBlur: 0, /** * @type {number} */ textShadowOffsetX: 0, /** * @type {number} */ textShadowOffsetY: 0, /** * @type {string} */ textBoxShadowColor: 'transparent', /** * @type {number} */ textBoxShadowBlur: 0, /** * @type {number} */ textBoxShadowOffsetX: 0, /** * @type {number} */ textBoxShadowOffsetY: 0, /** * Whether transform text. * Only available in Path and Image element, * where the text is called as `RectText`. * @type {boolean} */ transformText: false, /** * Text rotate around position of Path or Image. * The origin of the rotation can be specified by `textOrigin`. * Only available in Path and Image element, * where the text is called as `RectText`. */ textRotation: 0, /** * Text origin of text rotation. * Useful in the case like label rotation of circular symbol. * Only available in Path and Image element, where the text is called * as `RectText` and the element is called as "host element". * The value can be: * + If specified as a coordinate like `[10, 40]`, it is the `[x, y]` * base on the left-top corner of the rect of its host element. * + If specified as a string `center`, it is the center of the rect of * its host element. * + By default, this origin is the `textPosition`. * @type {string|Array.} */ textOrigin: null, /** * @type {string} */ textBackgroundColor: null, /** * @type {string} */ textBorderColor: null, /** * @type {number} */ textBorderWidth: 0, /** * @type {number} */ textBorderRadius: 0, /** * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]` * @type {number|Array.} */ textPadding: null, /** * Text styles for rich text. * @type {Object} */ rich: null, /** * {outerWidth, outerHeight, ellipsis, placeholder} * @type {Object} */ truncate: null, /** * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation * @type {string} */ blend: null, /** * @param {CanvasRenderingContext2D} ctx */ bind: function (ctx, el, prevEl) { var style = this; var prevStyle = prevEl && prevEl.style; // If no prevStyle, it means first draw. // Only apply cache if the last time cachced by this function. var notCheckCache = !prevStyle || ctx.__attrCachedBy !== ContextCachedBy.STYLE_BIND; ctx.__attrCachedBy = ContextCachedBy.STYLE_BIND; for (var i = 0; i < STYLE_COMMON_PROPS.length; i++) { var prop = STYLE_COMMON_PROPS[i]; var styleName = prop[0]; if (notCheckCache || style[styleName] !== prevStyle[styleName]) { // FIXME Invalid property value will cause style leak from previous element. ctx[styleName] = fixShadow(ctx, styleName, style[styleName] || prop[1]); } } if ((notCheckCache || style.fill !== prevStyle.fill)) { ctx.fillStyle = style.fill; } if ((notCheckCache || style.stroke !== prevStyle.stroke)) { ctx.strokeStyle = style.stroke; } if ((notCheckCache || style.opacity !== prevStyle.opacity)) { ctx.globalAlpha = style.opacity == null ? 1 : style.opacity; } if ((notCheckCache || style.blend !== prevStyle.blend)) { ctx.globalCompositeOperation = style.blend || 'source-over'; } if (this.hasStroke()) { var lineWidth = style.lineWidth; ctx.lineWidth = lineWidth / ( (this.strokeNoScale && el && el.getLineScale) ? el.getLineScale() : 1 ); } }, hasFill: function () { var fill = this.fill; return fill != null && fill !== 'none'; }, hasStroke: function () { var stroke = this.stroke; return stroke != null && stroke !== 'none' && this.lineWidth > 0; }, /** * Extend from other style * @param {zrender/graphic/Style} otherStyle * @param {boolean} overwrite true: overwrirte any way. * false: overwrite only when !target.hasOwnProperty * others: overwrite when property is not null/undefined. */ extendFrom: function (otherStyle, overwrite) { if (otherStyle) { for (var name in otherStyle) { if (otherStyle.hasOwnProperty(name) && (overwrite === true || ( overwrite === false ? !this.hasOwnProperty(name) : otherStyle[name] != null ) ) ) { this[name] = otherStyle[name]; } } } }, /** * Batch setting style with a given object * @param {Object|string} obj * @param {*} [obj] */ set: function (obj, value) { if (typeof obj === 'string') { this[obj] = value; } else { this.extendFrom(obj, true); } }, /** * Clone * @return {zrender/graphic/Style} [description] */ clone: function () { var newStyle = new this.constructor(); newStyle.extendFrom(this, true); return newStyle; }, getGradient: function (ctx, obj, rect) { var method = obj.type === 'radial' ? createRadialGradient : createLinearGradient; var canvasGradient = method(ctx, obj, rect); var colorStops = obj.colorStops; for (var i = 0; i < colorStops.length; i++) { canvasGradient.addColorStop( colorStops[i].offset, colorStops[i].color ); } return canvasGradient; } }; var styleProto = Style.prototype; for (var i = 0; i < STYLE_COMMON_PROPS.length; i++) { var prop = STYLE_COMMON_PROPS[i]; if (!(prop[0] in styleProto)) { styleProto[prop[0]] = prop[1]; } } // Provide for others Style.getGradient = styleProto.getGradient; var Pattern = function (image, repeat) { // Should do nothing more in this constructor. Because gradient can be // declard by `color: {image: ...}`, where this constructor will not be called. this.image = image; this.repeat = repeat; // Can be cloned this.type = 'pattern'; }; Pattern.prototype.getCanvasPattern = function (ctx) { return ctx.createPattern(this.image, this.repeat || 'repeat'); }; /** * @module zrender/Layer * @author pissang(https://www.github.com/pissang) */ function returnFalse() { return false; } /** * 创建dom * * @inner * @param {string} id dom id 待用 * @param {Painter} painter painter instance * @param {number} number */ function createDom(id, painter, dpr) { var newDom = createCanvas(); var width = painter.getWidth(); var height = painter.getHeight(); var newDomStyle = newDom.style; if (newDomStyle) { // In node or some other non-browser environment newDomStyle.position = 'absolute'; newDomStyle.left = 0; newDomStyle.top = 0; newDomStyle.width = width + 'px'; newDomStyle.height = height + 'px'; newDom.setAttribute('data-zr-dom-id', id); } newDom.width = width * dpr; newDom.height = height * dpr; return newDom; } /** * @alias module:zrender/Layer * @constructor * @extends module:zrender/mixin/Transformable * @param {string} id * @param {module:zrender/Painter} painter * @param {number} [dpr] */ var Layer = function (id, painter, dpr) { var dom; dpr = dpr || devicePixelRatio; if (typeof id === 'string') { dom = createDom(id, painter, dpr); } // Not using isDom because in node it will return false else if (isObject$1(id)) { dom = id; id = dom.id; } this.id = id; this.dom = dom; var domStyle = dom.style; if (domStyle) { // Not in node dom.onselectstart = returnFalse; // 避免页面选中的尴尬 domStyle['-webkit-user-select'] = 'none'; domStyle['user-select'] = 'none'; domStyle['-webkit-touch-callout'] = 'none'; domStyle['-webkit-tap-highlight-color'] = 'rgba(0,0,0,0)'; domStyle['padding'] = 0; // eslint-disable-line dot-notation domStyle['margin'] = 0; // eslint-disable-line dot-notation domStyle['border-width'] = 0; } this.domBack = null; this.ctxBack = null; this.painter = painter; this.config = null; // Configs /** * 每次清空画布的颜色 * @type {string} * @default 0 */ this.clearColor = 0; /** * 是否开启动态模糊 * @type {boolean} * @default false */ this.motionBlur = false; /** * 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显 * @type {number} * @default 0.7 */ this.lastFrameAlpha = 0.7; /** * Layer dpr * @type {number} */ this.dpr = dpr; }; Layer.prototype = { constructor: Layer, __dirty: true, __used: false, __drawIndex: 0, __startIndex: 0, __endIndex: 0, incremental: false, getElementCount: function () { return this.__endIndex - this.__startIndex; }, initContext: function () { this.ctx = this.dom.getContext('2d'); this.ctx.dpr = this.dpr; }, createBackBuffer: function () { var dpr = this.dpr; this.domBack = createDom('back-' + this.id, this.painter, dpr); this.ctxBack = this.domBack.getContext('2d'); if (dpr !== 1) { this.ctxBack.scale(dpr, dpr); } }, /** * @param {number} width * @param {number} height */ resize: function (width, height) { var dpr = this.dpr; var dom = this.dom; var domStyle = dom.style; var domBack = this.domBack; if (domStyle) { domStyle.width = width + 'px'; domStyle.height = height + 'px'; } dom.width = width * dpr; dom.height = height * dpr; if (domBack) { domBack.width = width * dpr; domBack.height = height * dpr; if (dpr !== 1) { this.ctxBack.scale(dpr, dpr); } } }, /** * 清空该层画布 * @param {boolean} [clearAll]=false Clear all with out motion blur * @param {Color} [clearColor] */ clear: function (clearAll, clearColor) { var dom = this.dom; var ctx = this.ctx; var width = dom.width; var height = dom.height; var clearColor = clearColor || this.clearColor; var haveMotionBLur = this.motionBlur && !clearAll; var lastFrameAlpha = this.lastFrameAlpha; var dpr = this.dpr; if (haveMotionBLur) { if (!this.domBack) { this.createBackBuffer(); } this.ctxBack.globalCompositeOperation = 'copy'; this.ctxBack.drawImage( dom, 0, 0, width / dpr, height / dpr ); } ctx.clearRect(0, 0, width, height); if (clearColor && clearColor !== 'transparent') { var clearColorGradientOrPattern; // Gradient if (clearColor.colorStops) { // Cache canvas gradient clearColorGradientOrPattern = clearColor.__canvasGradient || Style.getGradient(ctx, clearColor, { x: 0, y: 0, width: width, height: height }); clearColor.__canvasGradient = clearColorGradientOrPattern; } // Pattern else if (clearColor.image) { clearColorGradientOrPattern = Pattern.prototype.getCanvasPattern.call(clearColor, ctx); } ctx.save(); ctx.fillStyle = clearColorGradientOrPattern || clearColor; ctx.fillRect(0, 0, width, height); ctx.restore(); } if (haveMotionBLur) { var domBack = this.domBack; ctx.save(); ctx.globalAlpha = lastFrameAlpha; ctx.drawImage(domBack, 0, 0, width, height); ctx.restore(); } } }; var requestAnimationFrame = ( typeof window !== 'undefined' && ( (window.requestAnimationFrame && window.requestAnimationFrame.bind(window)) // https://github.com/ecomfe/zrender/issues/189#issuecomment-224919809 || (window.msRequestAnimationFrame && window.msRequestAnimationFrame.bind(window)) || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ) ) || function (func) { setTimeout(func, 16); }; var globalImageCache = new LRU(50); /** * @param {string|HTMLImageElement|HTMLCanvasElement|Canvas} newImageOrSrc * @return {HTMLImageElement|HTMLCanvasElement|Canvas} image */ function findExistImage(newImageOrSrc) { if (typeof newImageOrSrc === 'string') { var cachedImgObj = globalImageCache.get(newImageOrSrc); return cachedImgObj && cachedImgObj.image; } else { return newImageOrSrc; } } /** * Caution: User should cache loaded images, but not just count on LRU. * Consider if required images more than LRU size, will dead loop occur? * * @param {string|HTMLImageElement|HTMLCanvasElement|Canvas} newImageOrSrc * @param {HTMLImageElement|HTMLCanvasElement|Canvas} image Existent image. * @param {module:zrender/Element} [hostEl] For calling `dirty`. * @param {Function} [cb] params: (image, cbPayload) * @param {Object} [cbPayload] Payload on cb calling. * @return {HTMLImageElement|HTMLCanvasElement|Canvas} image */ function createOrUpdateImage(newImageOrSrc, image, hostEl, cb, cbPayload) { if (!newImageOrSrc) { return image; } else if (typeof newImageOrSrc === 'string') { // Image should not be loaded repeatly. if ((image && image.__zrImageSrc === newImageOrSrc) || !hostEl) { return image; } // Only when there is no existent image or existent image src // is different, this method is responsible for load. var cachedImgObj = globalImageCache.get(newImageOrSrc); var pendingWrap = {hostEl: hostEl, cb: cb, cbPayload: cbPayload}; if (cachedImgObj) { image = cachedImgObj.image; !isImageReady(image) && cachedImgObj.pending.push(pendingWrap); } else { image = new Image(); image.onload = image.onerror = imageOnLoad; globalImageCache.put( newImageOrSrc, image.__cachedImgObj = { image: image, pending: [pendingWrap] } ); image.src = image.__zrImageSrc = newImageOrSrc; } return image; } // newImageOrSrc is an HTMLImageElement or HTMLCanvasElement or Canvas else { return newImageOrSrc; } } function imageOnLoad() { var cachedImgObj = this.__cachedImgObj; this.onload = this.onerror = this.__cachedImgObj = null; for (var i = 0; i < cachedImgObj.pending.length; i++) { var pendingWrap = cachedImgObj.pending[i]; var cb = pendingWrap.cb; cb && cb(this, pendingWrap.cbPayload); pendingWrap.hostEl.dirty(); } cachedImgObj.pending.length = 0; } function isImageReady(image) { return image && image.width && image.height; } var textWidthCache = {}; var textWidthCacheCounter = 0; var TEXT_CACHE_MAX = 5000; var STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g; var DEFAULT_FONT$1 = '12px sans-serif'; // Avoid assign to an exported variable, for transforming to cjs. var methods$1 = {}; function $override$1(name, fn) { methods$1[name] = fn; } /** * @public * @param {string} text * @param {string} font * @return {number} width */ function getWidth(text, font) { font = font || DEFAULT_FONT$1; var key = text + ':' + font; if (textWidthCache[key]) { return textWidthCache[key]; } var textLines = (text + '').split('\n'); var width = 0; for (var i = 0, l = textLines.length; i < l; i++) { // textContain.measureText may be overrided in SVG or VML width = Math.max(measureText(textLines[i], font).width, width); } if (textWidthCacheCounter > TEXT_CACHE_MAX) { textWidthCacheCounter = 0; textWidthCache = {}; } textWidthCacheCounter++; textWidthCache[key] = width; return width; } /** * @public * @param {string} text * @param {string} font * @param {string} [textAlign='left'] * @param {string} [textVerticalAlign='top'] * @param {Array.} [textPadding] * @param {Object} [rich] * @param {Object} [truncate] * @return {Object} {x, y, width, height, lineHeight} */ function getBoundingRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) { return rich ? getRichTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) : getPlainTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, truncate); } function getPlainTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, truncate) { var contentBlock = parsePlainText(text, font, textPadding, textLineHeight, truncate); var outerWidth = getWidth(text, font); if (textPadding) { outerWidth += textPadding[1] + textPadding[3]; } var outerHeight = contentBlock.outerHeight; var x = adjustTextX(0, outerWidth, textAlign); var y = adjustTextY(0, outerHeight, textVerticalAlign); var rect = new BoundingRect(x, y, outerWidth, outerHeight); rect.lineHeight = contentBlock.lineHeight; return rect; } function getRichTextRect(text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate) { var contentBlock = parseRichText(text, { rich: rich, truncate: truncate, font: font, textAlign: textAlign, textPadding: textPadding, textLineHeight: textLineHeight }); var outerWidth = contentBlock.outerWidth; var outerHeight = contentBlock.outerHeight; var x = adjustTextX(0, outerWidth, textAlign); var y = adjustTextY(0, outerHeight, textVerticalAlign); return new BoundingRect(x, y, outerWidth, outerHeight); } /** * @public * @param {number} x * @param {number} width * @param {string} [textAlign='left'] * @return {number} Adjusted x. */ function adjustTextX(x, width, textAlign) { // FIXME Right to left language if (textAlign === 'right') { x -= width; } else if (textAlign === 'center') { x -= width / 2; } return x; } /** * @public * @param {number} y * @param {number} height * @param {string} [textVerticalAlign='top'] * @return {number} Adjusted y. */ function adjustTextY(y, height, textVerticalAlign) { if (textVerticalAlign === 'middle') { y -= height / 2; } else if (textVerticalAlign === 'bottom') { y -= height; } return y; } /** * Follow same interface to `Displayable.prototype.calculateTextPosition`. * @public * @param {Obejct} [out] Prepared out object. If not input, auto created in the method. * @param {module:zrender/graphic/Style} style where `textPosition` and `textDistance` are visited. * @param {Object} rect {x, y, width, height} Rect of the host elment, according to which the text positioned. * @return {Object} The input `out`. Set: {x, y, textAlign, textVerticalAlign} */ function calculateTextPosition(out, style, rect) { var textPosition = style.textPosition; var distance = style.textDistance; var x = rect.x; var y = rect.y; distance = distance || 0; var height = rect.height; var width = rect.width; var halfHeight = height / 2; var textAlign = 'left'; var textVerticalAlign = 'top'; switch (textPosition) { case 'left': x -= distance; y += halfHeight; textAlign = 'right'; textVerticalAlign = 'middle'; break; case 'right': x += distance + width; y += halfHeight; textVerticalAlign = 'middle'; break; case 'top': x += width / 2; y -= distance; textAlign = 'center'; textVerticalAlign = 'bottom'; break; case 'bottom': x += width / 2; y += height + distance; textAlign = 'center'; break; case 'inside': x += width / 2; y += halfHeight; textAlign = 'center'; textVerticalAlign = 'middle'; break; case 'insideLeft': x += distance; y += halfHeight; textVerticalAlign = 'middle'; break; case 'insideRight': x += width - distance; y += halfHeight; textAlign = 'right'; textVerticalAlign = 'middle'; break; case 'insideTop': x += width / 2; y += distance; textAlign = 'center'; break; case 'insideBottom': x += width / 2; y += height - distance; textAlign = 'center'; textVerticalAlign = 'bottom'; break; case 'insideTopLeft': x += distance; y += distance; break; case 'insideTopRight': x += width - distance; y += distance; textAlign = 'right'; break; case 'insideBottomLeft': x += distance; y += height - distance; textVerticalAlign = 'bottom'; break; case 'insideBottomRight': x += width - distance; y += height - distance; textAlign = 'right'; textVerticalAlign = 'bottom'; break; } out = out || {}; out.x = x; out.y = y; out.textAlign = textAlign; out.textVerticalAlign = textVerticalAlign; return out; } /** * To be removed. But still do not remove in case that some one has imported it. * @deprecated * @public * @param {stirng} textPosition * @param {Object} rect {x, y, width, height} * @param {number} distance * @return {Object} {x, y, textAlign, textVerticalAlign} */ /** * Show ellipsis if overflow. * * @public * @param {string} text * @param {string} containerWidth * @param {string} font * @param {number} [ellipsis='...'] * @param {Object} [options] * @param {number} [options.maxIterations=3] * @param {number} [options.minChar=0] If truncate result are less * then minChar, ellipsis will not show, which is * better for user hint in some cases. * @param {number} [options.placeholder=''] When all truncated, use the placeholder. * @return {string} */ function truncateText(text, containerWidth, font, ellipsis, options) { if (!containerWidth) { return ''; } var textLines = (text + '').split('\n'); options = prepareTruncateOptions(containerWidth, font, ellipsis, options); // FIXME // It is not appropriate that every line has '...' when truncate multiple lines. for (var i = 0, len = textLines.length; i < len; i++) { textLines[i] = truncateSingleLine(textLines[i], options); } return textLines.join('\n'); } function prepareTruncateOptions(containerWidth, font, ellipsis, options) { options = extend({}, options); options.font = font; var ellipsis = retrieve2(ellipsis, '...'); options.maxIterations = retrieve2(options.maxIterations, 2); var minChar = options.minChar = retrieve2(options.minChar, 0); // FIXME // Other languages? options.cnCharWidth = getWidth('国', font); // FIXME // Consider proportional font? var ascCharWidth = options.ascCharWidth = getWidth('a', font); options.placeholder = retrieve2(options.placeholder, ''); // Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'. // Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'. var contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap. for (var i = 0; i < minChar && contentWidth >= ascCharWidth; i++) { contentWidth -= ascCharWidth; } var ellipsisWidth = getWidth(ellipsis, font); if (ellipsisWidth > contentWidth) { ellipsis = ''; ellipsisWidth = 0; } contentWidth = containerWidth - ellipsisWidth; options.ellipsis = ellipsis; options.ellipsisWidth = ellipsisWidth; options.contentWidth = contentWidth; options.containerWidth = containerWidth; return options; } function truncateSingleLine(textLine, options) { var containerWidth = options.containerWidth; var font = options.font; var contentWidth = options.contentWidth; if (!containerWidth) { return ''; } var lineWidth = getWidth(textLine, font); if (lineWidth <= containerWidth) { return textLine; } for (var j = 0; ; j++) { if (lineWidth <= contentWidth || j >= options.maxIterations) { textLine += options.ellipsis; break; } var subLength = j === 0 ? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth) : lineWidth > 0 ? Math.floor(textLine.length * contentWidth / lineWidth) : 0; textLine = textLine.substr(0, subLength); lineWidth = getWidth(textLine, font); } if (textLine === '') { textLine = options.placeholder; } return textLine; } function estimateLength(text, contentWidth, ascCharWidth, cnCharWidth) { var width = 0; var i = 0; for (var len = text.length; i < len && width < contentWidth; i++) { var charCode = text.charCodeAt(i); width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth; } return i; } /** * @public * @param {string} font * @return {number} line height */ function getLineHeight(font) { // FIXME A rough approach. return getWidth('国', font); } /** * @public * @param {string} text * @param {string} font * @return {Object} width */ function measureText(text, font) { return methods$1.measureText(text, font); } // Avoid assign to an exported variable, for transforming to cjs. methods$1.measureText = function (text, font) { var ctx = getContext(); ctx.font = font || DEFAULT_FONT$1; return ctx.measureText(text); }; /** * @public * @param {string} text * @param {string} font * @param {Object} [truncate] * @return {Object} block: {lineHeight, lines, height, outerHeight, canCacheByTextString} * Notice: for performance, do not calculate outerWidth util needed. * `canCacheByTextString` means the result `lines` is only determined by the input `text`. * Thus we can simply comparing the `input` text to determin whether the result changed, * without travel the result `lines`. */ function parsePlainText(text, font, padding, textLineHeight, truncate) { text != null && (text += ''); var lineHeight = retrieve2(textLineHeight, getLineHeight(font)); var lines = text ? text.split('\n') : []; var height = lines.length * lineHeight; var outerHeight = height; var canCacheByTextString = true; if (padding) { outerHeight += padding[0] + padding[2]; } if (text && truncate) { canCacheByTextString = false; var truncOuterHeight = truncate.outerHeight; var truncOuterWidth = truncate.outerWidth; if (truncOuterHeight != null && outerHeight > truncOuterHeight) { text = ''; lines = []; } else if (truncOuterWidth != null) { var options = prepareTruncateOptions( truncOuterWidth - (padding ? padding[1] + padding[3] : 0), font, truncate.ellipsis, {minChar: truncate.minChar, placeholder: truncate.placeholder} ); // FIXME // It is not appropriate that every line has '...' when truncate multiple lines. for (var i = 0, len = lines.length; i < len; i++) { lines[i] = truncateSingleLine(lines[i], options); } } } return { lines: lines, height: height, outerHeight: outerHeight, lineHeight: lineHeight, canCacheByTextString: canCacheByTextString }; } /** * For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx' * Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'. * * @public * @param {string} text * @param {Object} style * @return {Object} block * { * width, * height, * lines: [{ * lineHeight, * width, * tokens: [[{ * styleName, * text, * width, // include textPadding * height, // include textPadding * textWidth, // pure text width * textHeight, // pure text height * lineHeihgt, * font, * textAlign, * textVerticalAlign * }], [...], ...] * }, ...] * } * If styleName is undefined, it is plain text. */ function parseRichText(text, style) { var contentBlock = {lines: [], width: 0, height: 0}; text != null && (text += ''); if (!text) { return contentBlock; } var lastIndex = STYLE_REG.lastIndex = 0; var result; while ((result = STYLE_REG.exec(text)) != null) { var matchedIndex = result.index; if (matchedIndex > lastIndex) { pushTokens(contentBlock, text.substring(lastIndex, matchedIndex)); } pushTokens(contentBlock, result[2], result[1]); lastIndex = STYLE_REG.lastIndex; } if (lastIndex < text.length) { pushTokens(contentBlock, text.substring(lastIndex, text.length)); } var lines = contentBlock.lines; var contentHeight = 0; var contentWidth = 0; // For `textWidth: 100%` var pendingList = []; var stlPadding = style.textPadding; var truncate = style.truncate; var truncateWidth = truncate && truncate.outerWidth; var truncateHeight = truncate && truncate.outerHeight; if (stlPadding) { truncateWidth != null && (truncateWidth -= stlPadding[1] + stlPadding[3]); truncateHeight != null && (truncateHeight -= stlPadding[0] + stlPadding[2]); } // Calculate layout info of tokens. for (var i = 0; i < lines.length; i++) { var line = lines[i]; var lineHeight = 0; var lineWidth = 0; for (var j = 0; j < line.tokens.length; j++) { var token = line.tokens[j]; var tokenStyle = token.styleName && style.rich[token.styleName] || {}; // textPadding should not inherit from style. var textPadding = token.textPadding = tokenStyle.textPadding; // textFont has been asigned to font by `normalizeStyle`. var font = token.font = tokenStyle.font || style.font; // textHeight can be used when textVerticalAlign is specified in token. var tokenHeight = token.textHeight = retrieve2( // textHeight should not be inherited, consider it can be specified // as box height of the block. tokenStyle.textHeight, getLineHeight(font) ); textPadding && (tokenHeight += textPadding[0] + textPadding[2]); token.height = tokenHeight; token.lineHeight = retrieve3( tokenStyle.textLineHeight, style.textLineHeight, tokenHeight ); token.textAlign = tokenStyle && tokenStyle.textAlign || style.textAlign; token.textVerticalAlign = tokenStyle && tokenStyle.textVerticalAlign || 'middle'; if (truncateHeight != null && contentHeight + token.lineHeight > truncateHeight) { return {lines: [], width: 0, height: 0}; } token.textWidth = getWidth(token.text, font); var tokenWidth = tokenStyle.textWidth; var tokenWidthNotSpecified = tokenWidth == null || tokenWidth === 'auto'; // Percent width, can be `100%`, can be used in drawing separate // line when box width is needed to be auto. if (typeof tokenWidth === 'string' && tokenWidth.charAt(tokenWidth.length - 1) === '%') { token.percentWidth = tokenWidth; pendingList.push(token); tokenWidth = 0; // Do not truncate in this case, because there is no user case // and it is too complicated. } else { if (tokenWidthNotSpecified) { tokenWidth = token.textWidth; // FIXME: If image is not loaded and textWidth is not specified, calling // `getBoundingRect()` will not get correct result. var textBackgroundColor = tokenStyle.textBackgroundColor; var bgImg = textBackgroundColor && textBackgroundColor.image; // Use cases: // (1) If image is not loaded, it will be loaded at render phase and call // `dirty()` and `textBackgroundColor.image` will be replaced with the loaded // image, and then the right size will be calculated here at the next tick. // See `graphic/helper/text.js`. // (2) If image loaded, and `textBackgroundColor.image` is image src string, // use `imageHelper.findExistImage` to find cached image. // `imageHelper.findExistImage` will always be called here before // `imageHelper.createOrUpdateImage` in `graphic/helper/text.js#renderRichText` // which ensures that image will not be rendered before correct size calcualted. if (bgImg) { bgImg = findExistImage(bgImg); if (isImageReady(bgImg)) { tokenWidth = Math.max(tokenWidth, bgImg.width * tokenHeight / bgImg.height); } } } var paddingW = textPadding ? textPadding[1] + textPadding[3] : 0; tokenWidth += paddingW; var remianTruncWidth = truncateWidth != null ? truncateWidth - lineWidth : null; if (remianTruncWidth != null && remianTruncWidth < tokenWidth) { if (!tokenWidthNotSpecified || remianTruncWidth < paddingW) { token.text = ''; token.textWidth = tokenWidth = 0; } else { token.text = truncateText( token.text, remianTruncWidth - paddingW, font, truncate.ellipsis, {minChar: truncate.minChar} ); token.textWidth = getWidth(token.text, font); tokenWidth = token.textWidth + paddingW; } } } lineWidth += (token.width = tokenWidth); tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight)); } line.width = lineWidth; line.lineHeight = lineHeight; contentHeight += lineHeight; contentWidth = Math.max(contentWidth, lineWidth); } contentBlock.outerWidth = contentBlock.width = retrieve2(style.textWidth, contentWidth); contentBlock.outerHeight = contentBlock.height = retrieve2(style.textHeight, contentHeight); if (stlPadding) { contentBlock.outerWidth += stlPadding[1] + stlPadding[3]; contentBlock.outerHeight += stlPadding[0] + stlPadding[2]; } for (var i = 0; i < pendingList.length; i++) { var token = pendingList[i]; var percentWidth = token.percentWidth; // Should not base on outerWidth, because token can not be placed out of padding. token.width = parseInt(percentWidth, 10) / 100 * contentWidth; } return contentBlock; } function pushTokens(block, str, styleName) { var isEmptyStr = str === ''; var strs = str.split('\n'); var lines = block.lines; for (var i = 0; i < strs.length; i++) { var text = strs[i]; var token = { styleName: styleName, text: text, isLineHolder: !text && !isEmptyStr }; // The first token should be appended to the last line. if (!i) { var tokens = (lines[lines.length - 1] || (lines[0] = {tokens: []})).tokens; // Consider cases: // (1) ''.split('\n') => ['', '\n', ''], the '' at the first item // (which is a placeholder) should be replaced by new token. // (2) A image backage, where token likes {a|}. // (3) A redundant '' will affect textAlign in line. // (4) tokens with the same tplName should not be merged, because // they should be displayed in different box (with border and padding). var tokensLen = tokens.length; (tokensLen === 1 && tokens[0].isLineHolder) ? (tokens[0] = token) // Consider text is '', only insert when it is the "lineHolder" or // "emptyStr". Otherwise a redundant '' will affect textAlign in line. : ((text || !tokensLen || isEmptyStr) && tokens.push(token)); } // Other tokens always start a new line. else { // If there is '', insert it as a placeholder. lines.push({tokens: [token]}); } } } function makeFont(style) { // FIXME in node-canvas fontWeight is before fontStyle // Use `fontSize` `fontFamily` to check whether font properties are defined. var font = (style.fontSize || style.fontFamily) && [ style.fontStyle, style.fontWeight, (style.fontSize || 12) + 'px', // If font properties are defined, `fontFamily` should not be ignored. style.fontFamily || 'sans-serif' ].join(' '); return font && trim(font) || style.textFont || style.font; } /** * @param {Object} ctx * @param {Object} shape * @param {number} shape.x * @param {number} shape.y * @param {number} shape.width * @param {number} shape.height * @param {number} shape.r */ function buildPath(ctx, shape) { var x = shape.x; var y = shape.y; var width = shape.width; var height = shape.height; var r = shape.r; var r1; var r2; var r3; var r4; // Convert width and height to positive for better borderRadius if (width < 0) { x = x + width; width = -width; } if (height < 0) { y = y + height; height = -height; } if (typeof r === 'number') { r1 = r2 = r3 = r4 = r; } else if (r instanceof Array) { if (r.length === 1) { r1 = r2 = r3 = r4 = r[0]; } else if (r.length === 2) { r1 = r3 = r[0]; r2 = r4 = r[1]; } else if (r.length === 3) { r1 = r[0]; r2 = r4 = r[1]; r3 = r[2]; } else { r1 = r[0]; r2 = r[1]; r3 = r[2]; r4 = r[3]; } } else { r1 = r2 = r3 = r4 = 0; } var total; if (r1 + r2 > width) { total = r1 + r2; r1 *= width / total; r2 *= width / total; } if (r3 + r4 > width) { total = r3 + r4; r3 *= width / total; r4 *= width / total; } if (r2 + r3 > height) { total = r2 + r3; r2 *= height / total; r3 *= height / total; } if (r1 + r4 > height) { total = r1 + r4; r1 *= height / total; r4 *= height / total; } ctx.moveTo(x + r1, y); ctx.lineTo(x + width - r2, y); r2 !== 0 && ctx.arc(x + width - r2, y + r2, r2, -Math.PI / 2, 0); ctx.lineTo(x + width, y + height - r3); r3 !== 0 && ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI / 2); ctx.lineTo(x + r4, y + height); r4 !== 0 && ctx.arc(x + r4, y + height - r4, r4, Math.PI / 2, Math.PI); ctx.lineTo(x, y + r1); r1 !== 0 && ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5); } var DEFAULT_FONT = DEFAULT_FONT$1; // TODO: Have not support 'start', 'end' yet. var VALID_TEXT_ALIGN = {left: 1, right: 1, center: 1}; var VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1}; // Different from `STYLE_COMMON_PROPS` of `graphic/Style`, // the default value of shadowColor is `'transparent'`. var SHADOW_STYLE_COMMON_PROPS = [ ['textShadowBlur', 'shadowBlur', 0], ['textShadowOffsetX', 'shadowOffsetX', 0], ['textShadowOffsetY', 'shadowOffsetY', 0], ['textShadowColor', 'shadowColor', 'transparent'] ]; var _tmpTextPositionResult = {}; var _tmpBoxPositionResult = {}; /** * @param {module:zrender/graphic/Style} style * @return {module:zrender/graphic/Style} The input style. */ function normalizeTextStyle(style) { normalizeStyle(style); each$1(style.rich, normalizeStyle); return style; } function normalizeStyle(style) { if (style) { style.font = makeFont(style); var textAlign = style.textAlign; textAlign === 'middle' && (textAlign = 'center'); style.textAlign = ( textAlign == null || VALID_TEXT_ALIGN[textAlign] ) ? textAlign : 'left'; // Compatible with textBaseline. var textVerticalAlign = style.textVerticalAlign || style.textBaseline; textVerticalAlign === 'center' && (textVerticalAlign = 'middle'); style.textVerticalAlign = ( textVerticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[textVerticalAlign] ) ? textVerticalAlign : 'top'; var textPadding = style.textPadding; if (textPadding) { style.textPadding = normalizeCssArray(style.textPadding); } } } /** * @param {CanvasRenderingContext2D} ctx * @param {string} text * @param {module:zrender/graphic/Style} style * @param {Object|boolean} [rect] {x, y, width, height} * If set false, rect text is not used. * @param {Element|module:zrender/graphic/helper/constant.WILL_BE_RESTORED} [prevEl] For ctx prop cache. */ function renderText(hostEl, ctx, text, style, rect, prevEl) { style.rich ? renderRichText(hostEl, ctx, text, style, rect, prevEl) : renderPlainText(hostEl, ctx, text, style, rect, prevEl); } // Avoid setting to ctx according to prevEl if possible for // performance in scenarios of large amount text. function renderPlainText(hostEl, ctx, text, style, rect, prevEl) { 'use strict'; var needDrawBg = needDrawBackground(style); var prevStyle; var checkCache = false; var cachedByMe = ctx.__attrCachedBy === ContextCachedBy.PLAIN_TEXT; // Only take and check cache for `Text` el, but not RectText. if (prevEl !== WILL_BE_RESTORED) { if (prevEl) { prevStyle = prevEl.style; checkCache = !needDrawBg && cachedByMe && prevStyle; } // Prevent from using cache in `Style::bind`, because of the case: // ctx property is modified by other properties than `Style::bind` // used, and Style::bind is called next. ctx.__attrCachedBy = needDrawBg ? ContextCachedBy.NONE : ContextCachedBy.PLAIN_TEXT; } // Since this will be restored, prevent from using these props to check cache in the next // entering of this method. But do not need to clear other cache like `Style::bind`. else if (cachedByMe) { ctx.__attrCachedBy = ContextCachedBy.NONE; } var styleFont = style.font || DEFAULT_FONT; // PENDING // Only `Text` el set `font` and keep it (`RectText` will restore). So theoretically // we can make font cache on ctx, which can cache for text el that are discontinuous. // But layer save/restore needed to be considered. // if (styleFont !== ctx.__fontCache) { // ctx.font = styleFont; // if (prevEl !== WILL_BE_RESTORED) { // ctx.__fontCache = styleFont; // } // } if (!checkCache || styleFont !== (prevStyle.font || DEFAULT_FONT)) { ctx.font = styleFont; } // Use the final font from context-2d, because the final // font might not be the style.font when it is illegal. // But get `ctx.font` might be time consuming. var computedFont = hostEl.__computedFont; if (hostEl.__styleFont !== styleFont) { hostEl.__styleFont = styleFont; computedFont = hostEl.__computedFont = ctx.font; } var textPadding = style.textPadding; var textLineHeight = style.textLineHeight; var contentBlock = hostEl.__textCotentBlock; if (!contentBlock || hostEl.__dirtyText) { contentBlock = hostEl.__textCotentBlock = parsePlainText( text, computedFont, textPadding, textLineHeight, style.truncate ); } var outerHeight = contentBlock.outerHeight; var textLines = contentBlock.lines; var lineHeight = contentBlock.lineHeight; var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect); var baseX = boxPos.baseX; var baseY = boxPos.baseY; var textAlign = boxPos.textAlign || 'left'; var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing. applyTextRotation(ctx, style, rect, baseX, baseY); var boxY = adjustTextY(baseY, outerHeight, textVerticalAlign); var textX = baseX; var textY = boxY; if (needDrawBg || textPadding) { // Consider performance, do not call getTextWidth util necessary. var textWidth = getWidth(text, computedFont); var outerWidth = textWidth; textPadding && (outerWidth += textPadding[1] + textPadding[3]); var boxX = adjustTextX(baseX, outerWidth, textAlign); needDrawBg && drawBackground(hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight); if (textPadding) { textX = getTextXForPadding(baseX, textAlign, textPadding); textY += textPadding[0]; } } // Always set textAlign and textBase line, because it is difficute to calculate // textAlign from prevEl, and we dont sure whether textAlign will be reset if // font set happened. ctx.textAlign = textAlign; // Force baseline to be "middle". Otherwise, if using "top", the // text will offset downward a little bit in font "Microsoft YaHei". ctx.textBaseline = 'middle'; // Set text opacity ctx.globalAlpha = style.opacity || 1; // Always set shadowBlur and shadowOffset to avoid leak from displayable. for (var i = 0; i < SHADOW_STYLE_COMMON_PROPS.length; i++) { var propItem = SHADOW_STYLE_COMMON_PROPS[i]; var styleProp = propItem[0]; var ctxProp = propItem[1]; var val = style[styleProp]; if (!checkCache || val !== prevStyle[styleProp]) { ctx[ctxProp] = fixShadow(ctx, ctxProp, val || propItem[2]); } } // `textBaseline` is set as 'middle'. textY += lineHeight / 2; var textStrokeWidth = style.textStrokeWidth; var textStrokeWidthPrev = checkCache ? prevStyle.textStrokeWidth : null; var strokeWidthChanged = !checkCache || textStrokeWidth !== textStrokeWidthPrev; var strokeChanged = !checkCache || strokeWidthChanged || style.textStroke !== prevStyle.textStroke; var textStroke = getStroke(style.textStroke, textStrokeWidth); var textFill = getFill(style.textFill); if (textStroke) { if (strokeWidthChanged) { ctx.lineWidth = textStrokeWidth; } if (strokeChanged) { ctx.strokeStyle = textStroke; } } if (textFill) { if (!checkCache || style.textFill !== prevStyle.textFill) { ctx.fillStyle = textFill; } } // Optimize simply, in most cases only one line exists. if (textLines.length === 1) { // Fill after stroke so the outline will not cover the main part. textStroke && ctx.strokeText(textLines[0], textX, textY); textFill && ctx.fillText(textLines[0], textX, textY); } else { for (var i = 0; i < textLines.length; i++) { // Fill after stroke so the outline will not cover the main part. textStroke && ctx.strokeText(textLines[i], textX, textY); textFill && ctx.fillText(textLines[i], textX, textY); textY += lineHeight; } } } function renderRichText(hostEl, ctx, text, style, rect, prevEl) { // Do not do cache for rich text because of the complexity. // But `RectText` this will be restored, do not need to clear other cache like `Style::bind`. if (prevEl !== WILL_BE_RESTORED) { ctx.__attrCachedBy = ContextCachedBy.NONE; } var contentBlock = hostEl.__textCotentBlock; if (!contentBlock || hostEl.__dirtyText) { contentBlock = hostEl.__textCotentBlock = parseRichText(text, style); } drawRichText(hostEl, ctx, contentBlock, style, rect); } function drawRichText(hostEl, ctx, contentBlock, style, rect) { var contentWidth = contentBlock.width; var outerWidth = contentBlock.outerWidth; var outerHeight = contentBlock.outerHeight; var textPadding = style.textPadding; var boxPos = getBoxPosition(_tmpBoxPositionResult, hostEl, style, rect); var baseX = boxPos.baseX; var baseY = boxPos.baseY; var textAlign = boxPos.textAlign; var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing. applyTextRotation(ctx, style, rect, baseX, baseY); var boxX = adjustTextX(baseX, outerWidth, textAlign); var boxY = adjustTextY(baseY, outerHeight, textVerticalAlign); var xLeft = boxX; var lineTop = boxY; if (textPadding) { xLeft += textPadding[3]; lineTop += textPadding[0]; } var xRight = xLeft + contentWidth; needDrawBackground(style) && drawBackground( hostEl, ctx, style, boxX, boxY, outerWidth, outerHeight ); for (var i = 0; i < contentBlock.lines.length; i++) { var line = contentBlock.lines[i]; var tokens = line.tokens; var tokenCount = tokens.length; var lineHeight = line.lineHeight; var usedWidth = line.width; var leftIndex = 0; var lineXLeft = xLeft; var lineXRight = xRight; var rightIndex = tokenCount - 1; var token; while ( leftIndex < tokenCount && (token = tokens[leftIndex], !token.textAlign || token.textAlign === 'left') ) { placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft, 'left'); usedWidth -= token.width; lineXLeft += token.width; leftIndex++; } while ( rightIndex >= 0 && (token = tokens[rightIndex], token.textAlign === 'right') ) { placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXRight, 'right'); usedWidth -= token.width; lineXRight -= token.width; rightIndex--; } // The other tokens are placed as textAlign 'center' if there is enough space. lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - usedWidth) / 2; while (leftIndex <= rightIndex) { token = tokens[leftIndex]; // Consider width specified by user, use 'center' rather than 'left'. placeToken(hostEl, ctx, token, style, lineHeight, lineTop, lineXLeft + token.width / 2, 'center'); lineXLeft += token.width; leftIndex++; } lineTop += lineHeight; } } function applyTextRotation(ctx, style, rect, x, y) { // textRotation only apply in RectText. if (rect && style.textRotation) { var origin = style.textOrigin; if (origin === 'center') { x = rect.width / 2 + rect.x; y = rect.height / 2 + rect.y; } else if (origin) { x = origin[0] + rect.x; y = origin[1] + rect.y; } ctx.translate(x, y); // Positive: anticlockwise ctx.rotate(-style.textRotation); ctx.translate(-x, -y); } } function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign) { var tokenStyle = style.rich[token.styleName] || {}; tokenStyle.text = token.text; // 'ctx.textBaseline' is always set as 'middle', for sake of // the bias of "Microsoft YaHei". var textVerticalAlign = token.textVerticalAlign; var y = lineTop + lineHeight / 2; if (textVerticalAlign === 'top') { y = lineTop + token.height / 2; } else if (textVerticalAlign === 'bottom') { y = lineTop + lineHeight - token.height / 2; } !token.isLineHolder && needDrawBackground(tokenStyle) && drawBackground( hostEl, ctx, tokenStyle, textAlign === 'right' ? x - token.width : textAlign === 'center' ? x - token.width / 2 : x, y - token.height / 2, token.width, token.height ); var textPadding = token.textPadding; if (textPadding) { x = getTextXForPadding(x, textAlign, textPadding); y -= token.height / 2 - textPadding[2] - token.textHeight / 2; } setCtx(ctx, 'shadowBlur', retrieve3(tokenStyle.textShadowBlur, style.textShadowBlur, 0)); setCtx(ctx, 'shadowColor', tokenStyle.textShadowColor || style.textShadowColor || 'transparent'); setCtx(ctx, 'shadowOffsetX', retrieve3(tokenStyle.textShadowOffsetX, style.textShadowOffsetX, 0)); setCtx(ctx, 'shadowOffsetY', retrieve3(tokenStyle.textShadowOffsetY, style.textShadowOffsetY, 0)); setCtx(ctx, 'textAlign', textAlign); // Force baseline to be "middle". Otherwise, if using "top", the // text will offset downward a little bit in font "Microsoft YaHei". setCtx(ctx, 'textBaseline', 'middle'); setCtx(ctx, 'font', token.font || DEFAULT_FONT); var textStroke = getStroke(tokenStyle.textStroke || style.textStroke, textStrokeWidth); var textFill = getFill(tokenStyle.textFill || style.textFill); var textStrokeWidth = retrieve2(tokenStyle.textStrokeWidth, style.textStrokeWidth); // Fill after stroke so the outline will not cover the main part. if (textStroke) { setCtx(ctx, 'lineWidth', textStrokeWidth); setCtx(ctx, 'strokeStyle', textStroke); ctx.strokeText(token.text, x, y); } if (textFill) { setCtx(ctx, 'fillStyle', textFill); ctx.fillText(token.text, x, y); } } function needDrawBackground(style) { return !!( style.textBackgroundColor || (style.textBorderWidth && style.textBorderColor) ); } // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius, text} // shape: {x, y, width, height} function drawBackground(hostEl, ctx, style, x, y, width, height) { var textBackgroundColor = style.textBackgroundColor; var textBorderWidth = style.textBorderWidth; var textBorderColor = style.textBorderColor; var isPlainBg = isString(textBackgroundColor); setCtx(ctx, 'shadowBlur', style.textBoxShadowBlur || 0); setCtx(ctx, 'shadowColor', style.textBoxShadowColor || 'transparent'); setCtx(ctx, 'shadowOffsetX', style.textBoxShadowOffsetX || 0); setCtx(ctx, 'shadowOffsetY', style.textBoxShadowOffsetY || 0); if (isPlainBg || (textBorderWidth && textBorderColor)) { ctx.beginPath(); var textBorderRadius = style.textBorderRadius; if (!textBorderRadius) { ctx.rect(x, y, width, height); } else { buildPath(ctx, { x: x, y: y, width: width, height: height, r: textBorderRadius }); } ctx.closePath(); } if (isPlainBg) { setCtx(ctx, 'fillStyle', textBackgroundColor); if (style.fillOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.fillOpacity * style.opacity; ctx.fill(); ctx.globalAlpha = originalGlobalAlpha; } else { ctx.fill(); } } else if (isObject$1(textBackgroundColor)) { var image = textBackgroundColor.image; image = createOrUpdateImage( image, null, hostEl, onBgImageLoaded, textBackgroundColor ); if (image && isImageReady(image)) { ctx.drawImage(image, x, y, width, height); } } if (textBorderWidth && textBorderColor) { setCtx(ctx, 'lineWidth', textBorderWidth); setCtx(ctx, 'strokeStyle', textBorderColor); if (style.strokeOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.strokeOpacity * style.opacity; ctx.stroke(); ctx.globalAlpha = originalGlobalAlpha; } else { ctx.stroke(); } } } function onBgImageLoaded(image, textBackgroundColor) { // Replace image, so that `contain/text.js#parseRichText` // will get correct result in next tick. textBackgroundColor.image = image; } function getBoxPosition(out, hostEl, style, rect) { var baseX = style.x || 0; var baseY = style.y || 0; var textAlign = style.textAlign; var textVerticalAlign = style.textVerticalAlign; // Text position represented by coord if (rect) { var textPosition = style.textPosition; if (textPosition instanceof Array) { // Percent baseX = rect.x + parsePercent(textPosition[0], rect.width); baseY = rect.y + parsePercent(textPosition[1], rect.height); } else { var res = (hostEl && hostEl.calculateTextPosition) ? hostEl.calculateTextPosition(_tmpTextPositionResult, style, rect) : calculateTextPosition(_tmpTextPositionResult, style, rect); baseX = res.x; baseY = res.y; // Default align and baseline when has textPosition textAlign = textAlign || res.textAlign; textVerticalAlign = textVerticalAlign || res.textVerticalAlign; } // textOffset is only support in RectText, otherwise // we have to adjust boundingRect for textOffset. var textOffset = style.textOffset; if (textOffset) { baseX += textOffset[0]; baseY += textOffset[1]; } } out = out || {}; out.baseX = baseX; out.baseY = baseY; out.textAlign = textAlign; out.textVerticalAlign = textVerticalAlign; return out; } function setCtx(ctx, prop, value) { ctx[prop] = fixShadow(ctx, prop, value); return ctx[prop]; } /** * @param {string} [stroke] If specified, do not check style.textStroke. * @param {string} [lineWidth] If specified, do not check style.textStroke. * @param {number} style */ function getStroke(stroke, lineWidth) { return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none') ? null // TODO pattern and gradient? : (stroke.image || stroke.colorStops) ? '#000' : stroke; } function getFill(fill) { return (fill == null || fill === 'none') ? null // TODO pattern and gradient? : (fill.image || fill.colorStops) ? '#000' : fill; } function parsePercent(value, maxValue) { if (typeof value === 'string') { if (value.lastIndexOf('%') >= 0) { return parseFloat(value) / 100 * maxValue; } return parseFloat(value); } return value; } function getTextXForPadding(x, textAlign, textPadding) { return textAlign === 'right' ? (x - textPadding[1]) : textAlign === 'center' ? (x + textPadding[3] / 2 - textPadding[1] / 2) : (x + textPadding[3]); } /** * @param {string} text * @param {module:zrender/Style} style * @return {boolean} */ function needDrawText(text, style) { return text != null && (text || style.textBackgroundColor || (style.textBorderWidth && style.textBorderColor) || style.textPadding ); } /** * Mixin for drawing text in a element bounding rect * @module zrender/mixin/RectText */ var tmpRect$1 = new BoundingRect(); var RectText = function () {}; RectText.prototype = { constructor: RectText, /** * Draw text in a rect with specified position. * @param {CanvasRenderingContext2D} ctx * @param {Object} rect Displayable rect */ drawRectText: function (ctx, rect) { var style = this.style; rect = style.textRect || rect; // Optimize, avoid normalize every time. this.__dirty && normalizeTextStyle(style, true); var text = style.text; // Convert to string text != null && (text += ''); if (!needDrawText(text, style)) { return; } // FIXME // Do not provide prevEl to `textHelper.renderText` for ctx prop cache, // but use `ctx.save()` and `ctx.restore()`. Because the cache for rect // text propably break the cache for its host elements. ctx.save(); // Transform rect to view space var transform = this.transform; if (!style.transformText) { if (transform) { tmpRect$1.copy(rect); tmpRect$1.applyTransform(transform); rect = tmpRect$1; } } else { this.setTransform(ctx); } // transformText and textRotation can not be used at the same time. renderText(this, ctx, text, style, rect, WILL_BE_RESTORED); ctx.restore(); } }; /** * Base class of all displayable graphic objects * @module zrender/graphic/Displayable */ /** * @alias module:zrender/graphic/Displayable * @extends module:zrender/Element * @extends module:zrender/graphic/mixin/RectText */ function Displayable(opts) { opts = opts || {}; Element.call(this, opts); // Extend properties for (var name in opts) { if ( opts.hasOwnProperty(name) && name !== 'style' ) { this[name] = opts[name]; } } /** * @type {module:zrender/graphic/Style} */ this.style = new Style(opts.style, this); this._rect = null; // Shapes for cascade clipping. // Can only be `null`/`undefined` or an non-empty array, MUST NOT be an empty array. // because it is easy to only using null to check whether clipPaths changed. this.__clipPaths = null; // FIXME Stateful must be mixined after style is setted // Stateful.call(this, opts); } Displayable.prototype = { constructor: Displayable, type: 'displayable', /** * Dirty flag. From which painter will determine if this displayable object needs brush. * @name module:zrender/graphic/Displayable#__dirty * @type {boolean} */ __dirty: true, /** * Whether the displayable object is visible. when it is true, the displayable object * is not drawn, but the mouse event can still trigger the object. * @name module:/zrender/graphic/Displayable#invisible * @type {boolean} * @default false */ invisible: false, /** * @name module:/zrender/graphic/Displayable#z * @type {number} * @default 0 */ z: 0, /** * @name module:/zrender/graphic/Displayable#z * @type {number} * @default 0 */ z2: 0, /** * The z level determines the displayable object can be drawn in which layer canvas. * @name module:/zrender/graphic/Displayable#zlevel * @type {number} * @default 0 */ zlevel: 0, /** * Whether it can be dragged. * @name module:/zrender/graphic/Displayable#draggable * @type {boolean} * @default false */ draggable: false, /** * Whether is it dragging. * @name module:/zrender/graphic/Displayable#draggable * @type {boolean} * @default false */ dragging: false, /** * Whether to respond to mouse events. * @name module:/zrender/graphic/Displayable#silent * @type {boolean} * @default false */ silent: false, /** * If enable culling * @type {boolean} * @default false */ culling: false, /** * Mouse cursor when hovered * @name module:/zrender/graphic/Displayable#cursor * @type {string} */ cursor: 'pointer', /** * If hover area is bounding rect * @name module:/zrender/graphic/Displayable#rectHover * @type {string} */ rectHover: false, /** * Render the element progressively when the value >= 0, * usefull for large data. * @type {boolean} */ progressive: false, /** * @type {boolean} */ incremental: false, /** * Scale ratio for global scale. * @type {boolean} */ globalScaleRatio: 1, beforeBrush: function (ctx) {}, afterBrush: function (ctx) {}, /** * Graphic drawing method. * @param {CanvasRenderingContext2D} ctx */ // Interface brush: function (ctx, prevEl) {}, /** * Get the minimum bounding box. * @return {module:zrender/core/BoundingRect} */ // Interface getBoundingRect: function () {}, /** * If displayable element contain coord x, y * @param {number} x * @param {number} y * @return {boolean} */ contain: function (x, y) { return this.rectContain(x, y); }, /** * @param {Function} cb * @param {} context */ traverse: function (cb, context) { cb.call(context, this); }, /** * If bounding rect of element contain coord x, y * @param {number} x * @param {number} y * @return {boolean} */ rectContain: function (x, y) { var coord = this.transformCoordToLocal(x, y); var rect = this.getBoundingRect(); return rect.contain(coord[0], coord[1]); }, /** * Mark displayable element dirty and refresh next frame */ dirty: function () { this.__dirty = this.__dirtyText = true; this._rect = null; this.__zr && this.__zr.refresh(); }, /** * If displayable object binded any event * @return {boolean} */ // TODO, events bound by bind // isSilent: function () { // return !( // this.hoverable || this.draggable // || this.onmousemove || this.onmouseover || this.onmouseout // || this.onmousedown || this.onmouseup || this.onclick // || this.ondragenter || this.ondragover || this.ondragleave // || this.ondrop // ); // }, /** * Alias for animate('style') * @param {boolean} loop */ animateStyle: function (loop) { return this.animate('style', loop); }, attrKV: function (key, value) { if (key !== 'style') { Element.prototype.attrKV.call(this, key, value); } else { this.style.set(value); } }, /** * @param {Object|string} key * @param {*} value */ setStyle: function (key, value) { this.style.set(key, value); this.dirty(false); return this; }, /** * Use given style object * @param {Object} obj */ useStyle: function (obj) { this.style = new Style(obj, this); this.dirty(false); return this; }, /** * The string value of `textPosition` needs to be calculated to a real postion. * For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]` * by default. See `contain/text.js#calculateTextPosition` for more details. * But some coutom shapes like "pin", "flag" have center that is not exactly * `[width/2, height/2]`. So we provide this hook to customize the calculation * for those shapes. It will be called if the `style.textPosition` is a string. * @param {Obejct} [out] Prepared out object. If not provided, this method should * be responsible for creating one. * @param {module:zrender/graphic/Style} style * @param {Object} rect {x, y, width, height} * @return {Obejct} out The same as the input out. * { * x: number. mandatory. * y: number. mandatory. * textAlign: string. optional. use style.textAlign by default. * textVerticalAlign: string. optional. use style.textVerticalAlign by default. * } */ calculateTextPosition: null }; inherits(Displayable, Element); mixin(Displayable, RectText); /** * @alias zrender/graphic/Image * @extends module:zrender/graphic/Displayable * @constructor * @param {Object} opts */ function ZImage(opts) { Displayable.call(this, opts); } ZImage.prototype = { constructor: ZImage, type: 'image', brush: function (ctx, prevEl) { var style = this.style; var src = style.image; // Must bind each time style.bind(ctx, this, prevEl); var image = this._image = createOrUpdateImage( src, this._image, this, this.onload ); if (!image || !isImageReady(image)) { return; } // 图片已经加载完成 // if (image.nodeName.toUpperCase() == 'IMG') { // if (!image.complete) { // return; // } // } // Else is canvas var x = style.x || 0; var y = style.y || 0; var width = style.width; var height = style.height; var aspect = image.width / image.height; if (width == null && height != null) { // Keep image/height ratio width = height * aspect; } else if (height == null && width != null) { height = width / aspect; } else if (width == null && height == null) { width = image.width; height = image.height; } // 设置transform this.setTransform(ctx); if (style.sWidth && style.sHeight) { var sx = style.sx || 0; var sy = style.sy || 0; ctx.drawImage( image, sx, sy, style.sWidth, style.sHeight, x, y, width, height ); } else if (style.sx && style.sy) { var sx = style.sx; var sy = style.sy; var sWidth = width - sx; var sHeight = height - sy; ctx.drawImage( image, sx, sy, sWidth, sHeight, x, y, width, height ); } else { ctx.drawImage(image, x, y, width, height); } // Draw rect text if (style.text != null) { // Only restore transform when needs draw text. this.restoreTransform(ctx); this.drawRectText(ctx, this.getBoundingRect()); } }, getBoundingRect: function () { var style = this.style; if (!this._rect) { this._rect = new BoundingRect( style.x || 0, style.y || 0, style.width || 0, style.height || 0 ); } return this._rect; } }; inherits(ZImage, Displayable); var HOVER_LAYER_ZLEVEL = 1e5; var CANVAS_ZLEVEL = 314159; var EL_AFTER_INCREMENTAL_INC = 0.01; var INCREMENTAL_INC = 0.001; function parseInt10(val) { return parseInt(val, 10); } function isLayerValid(layer) { if (!layer) { return false; } if (layer.__builtin__) { return true; } if (typeof (layer.resize) !== 'function' || typeof (layer.refresh) !== 'function' ) { return false; } return true; } var tmpRect = new BoundingRect(0, 0, 0, 0); var viewRect = new BoundingRect(0, 0, 0, 0); function isDisplayableCulled(el, width, height) { tmpRect.copy(el.getBoundingRect()); if (el.transform) { tmpRect.applyTransform(el.transform); } viewRect.width = width; viewRect.height = height; return !tmpRect.intersect(viewRect); } function isClipPathChanged(clipPaths, prevClipPaths) { // displayable.__clipPaths can only be `null`/`undefined` or an non-empty array. if (clipPaths === prevClipPaths) { return false; } if (!clipPaths || !prevClipPaths || (clipPaths.length !== prevClipPaths.length)) { return true; } for (var i = 0; i < clipPaths.length; i++) { if (clipPaths[i] !== prevClipPaths[i]) { return true; } } return false; } function doClip(clipPaths, ctx) { for (var i = 0; i < clipPaths.length; i++) { var clipPath = clipPaths[i]; clipPath.setTransform(ctx); ctx.beginPath(); clipPath.buildPath(ctx, clipPath.shape); ctx.clip(); // Transform back clipPath.restoreTransform(ctx); } } function createRoot(width, height) { var domRoot = document.createElement('div'); // domRoot.onselectstart = returnFalse; // Avoid page selected domRoot.style.cssText = [ 'position:relative', // IOS13 safari probably has a compositing bug (z order of the canvas and the consequent // dom does not act as expected) when some of the parent dom has // `-webkit-overflow-scrolling: touch;` and the webpage is longer than one screen and // the canvas is not at the top part of the page. // Check `https://bugs.webkit.org/show_bug.cgi?id=203681` for more details. We remove // this `overflow:hidden` to avoid the bug. // 'overflow:hidden', 'width:' + width + 'px', 'height:' + height + 'px', 'padding:0', 'margin:0', 'border-width:0' ].join(';') + ';'; return domRoot; } /** * @alias module:zrender/Painter * @constructor * @param {HTMLElement} root 绘图容器 * @param {module:zrender/Storage} storage * @param {Object} opts */ var Painter = function (root, storage, opts) { this.type = 'canvas'; // In node environment using node-canvas var singleCanvas = !root.nodeName // In node ? || root.nodeName.toUpperCase() === 'CANVAS'; this._opts = opts = extend({}, opts || {}); /** * @type {number} */ this.dpr = opts.devicePixelRatio || devicePixelRatio; /** * @type {boolean} * @private */ this._singleCanvas = singleCanvas; /** * 绘图容器 * @type {HTMLElement} */ this.root = root; var rootStyle = root.style; if (rootStyle) { rootStyle['-webkit-tap-highlight-color'] = 'transparent'; rootStyle['-webkit-user-select'] = rootStyle['user-select'] = rootStyle['-webkit-touch-callout'] = 'none'; root.innerHTML = ''; } /** * @type {module:zrender/Storage} */ this.storage = storage; /** * @type {Array.} * @private */ var zlevelList = this._zlevelList = []; /** * @type {Object.} * @private */ var layers = this._layers = {}; /** * @type {Object.} * @private */ this._layerConfig = {}; /** * zrender will do compositing when root is a canvas and have multiple zlevels. */ this._needsManuallyCompositing = false; if (!singleCanvas) { this._width = this._getSize(0); this._height = this._getSize(1); var domRoot = this._domRoot = createRoot( this._width, this._height ); root.appendChild(domRoot); } else { var width = root.width; var height = root.height; if (opts.width != null) { width = opts.width; } if (opts.height != null) { height = opts.height; } this.dpr = opts.devicePixelRatio || 1; // Use canvas width and height directly root.width = width * this.dpr; root.height = height * this.dpr; this._width = width; this._height = height; // Create layer if only one given canvas // Device can be specified to create a high dpi image. var mainLayer = new Layer(root, this, this.dpr); mainLayer.__builtin__ = true; mainLayer.initContext(); // FIXME Use canvas width and height // mainLayer.resize(width, height); layers[CANVAS_ZLEVEL] = mainLayer; mainLayer.zlevel = CANVAS_ZLEVEL; // Not use common zlevel. zlevelList.push(CANVAS_ZLEVEL); this._domRoot = root; } /** * @type {module:zrender/Layer} * @private */ this._hoverlayer = null; this._hoverElements = []; }; Painter.prototype = { constructor: Painter, getType: function () { return 'canvas'; }, /** * If painter use a single canvas * @return {boolean} */ isSingleCanvas: function () { return this._singleCanvas; }, /** * @return {HTMLDivElement} */ getViewportRoot: function () { return this._domRoot; }, getViewportRootOffset: function () { var viewportRoot = this.getViewportRoot(); if (viewportRoot) { return { offsetLeft: viewportRoot.offsetLeft || 0, offsetTop: viewportRoot.offsetTop || 0 }; } }, /** * 刷新 * @param {boolean} [paintAll=false] 强制绘制所有displayable */ refresh: function (paintAll) { var list = this.storage.getDisplayList(true); var zlevelList = this._zlevelList; this._redrawId = Math.random(); this._paintList(list, paintAll, this._redrawId); // Paint custum layers for (var i = 0; i < zlevelList.length; i++) { var z = zlevelList[i]; var layer = this._layers[z]; if (!layer.__builtin__ && layer.refresh) { var clearColor = i === 0 ? this._backgroundColor : null; layer.refresh(clearColor); } } this.refreshHover(); return this; }, addHover: function (el, hoverStyle) { if (el.__hoverMir) { return; } var elMirror = new el.constructor({ style: el.style, shape: el.shape, z: el.z, z2: el.z2, silent: el.silent }); elMirror.__from = el; el.__hoverMir = elMirror; hoverStyle && elMirror.setStyle(hoverStyle); this._hoverElements.push(elMirror); return elMirror; }, removeHover: function (el) { var elMirror = el.__hoverMir; var hoverElements = this._hoverElements; var idx = indexOf(hoverElements, elMirror); if (idx >= 0) { hoverElements.splice(idx, 1); } el.__hoverMir = null; }, clearHover: function (el) { var hoverElements = this._hoverElements; for (var i = 0; i < hoverElements.length; i++) { var from = hoverElements[i].__from; if (from) { from.__hoverMir = null; } } hoverElements.length = 0; }, refreshHover: function () { var hoverElements = this._hoverElements; var len = hoverElements.length; var hoverLayer = this._hoverlayer; hoverLayer && hoverLayer.clear(); if (!len) { return; } sort(hoverElements, this.storage.displayableSortFunc); // Use a extream large zlevel // FIXME? if (!hoverLayer) { hoverLayer = this._hoverlayer = this.getLayer(HOVER_LAYER_ZLEVEL); } var scope = {}; hoverLayer.ctx.save(); for (var i = 0; i < len;) { var el = hoverElements[i]; var originalEl = el.__from; // Original el is removed // PENDING if (!(originalEl && originalEl.__zr)) { hoverElements.splice(i, 1); originalEl.__hoverMir = null; len--; continue; } i++; // Use transform // FIXME style and shape ? if (!originalEl.invisible) { el.transform = originalEl.transform; el.invTransform = originalEl.invTransform; el.__clipPaths = originalEl.__clipPaths; // el. this._doPaintEl(el, hoverLayer, true, scope); } } hoverLayer.ctx.restore(); }, getHoverLayer: function () { return this.getLayer(HOVER_LAYER_ZLEVEL); }, _paintList: function (list, paintAll, redrawId) { if (this._redrawId !== redrawId) { return; } paintAll = paintAll || false; this._updateLayerStatus(list); var finished = this._doPaintList(list, paintAll); if (this._needsManuallyCompositing) { this._compositeManually(); } if (!finished) { var self = this; requestAnimationFrame(function () { self._paintList(list, paintAll, redrawId); }); } }, _compositeManually: function () { var ctx = this.getLayer(CANVAS_ZLEVEL).ctx; var width = this._domRoot.width; var height = this._domRoot.height; ctx.clearRect(0, 0, width, height); // PENDING, If only builtin layer? this.eachBuiltinLayer(function (layer) { if (layer.virtual) { ctx.drawImage(layer.dom, 0, 0, width, height); } }); }, _doPaintList: function (list, paintAll) { var layerList = []; for (var zi = 0; zi < this._zlevelList.length; zi++) { var zlevel = this._zlevelList[zi]; var layer = this._layers[zlevel]; if (layer.__builtin__ && layer !== this._hoverlayer && (layer.__dirty || paintAll) ) { layerList.push(layer); } } var finished = true; for (var k = 0; k < layerList.length; k++) { var layer = layerList[k]; var ctx = layer.ctx; var scope = {}; ctx.save(); var start = paintAll ? layer.__startIndex : layer.__drawIndex; var useTimer = !paintAll && layer.incremental && Date.now; var startTime = useTimer && Date.now(); var clearColor = layer.zlevel === this._zlevelList[0] ? this._backgroundColor : null; // All elements in this layer are cleared. if (layer.__startIndex === layer.__endIndex) { layer.clear(false, clearColor); } else if (start === layer.__startIndex) { var firstEl = list[start]; if (!firstEl.incremental || !firstEl.notClear || paintAll) { layer.clear(false, clearColor); } } if (start === -1) { console.error('For some unknown reason. drawIndex is -1'); start = layer.__startIndex; } for (var i = start; i < layer.__endIndex; i++) { var el = list[i]; this._doPaintEl(el, layer, paintAll, scope); el.__dirty = el.__dirtyText = false; if (useTimer) { // Date.now can be executed in 13,025,305 ops/second. var dTime = Date.now() - startTime; // Give 15 millisecond to draw. // The rest elements will be drawn in the next frame. if (dTime > 15) { break; } } } layer.__drawIndex = i; if (layer.__drawIndex < layer.__endIndex) { finished = false; } if (scope.prevElClipPaths) { // Needs restore the state. If last drawn element is in the clipping area. ctx.restore(); } ctx.restore(); } if (env$1.wxa) { // Flush for weixin application each$1(this._layers, function (layer) { if (layer && layer.ctx && layer.ctx.draw) { layer.ctx.draw(); } }); } return finished; }, _doPaintEl: function (el, currentLayer, forcePaint, scope) { var ctx = currentLayer.ctx; var m = el.transform; if ( (currentLayer.__dirty || forcePaint) // Ignore invisible element && !el.invisible // Ignore transparent element && el.style.opacity !== 0 // Ignore scale 0 element, in some environment like node-canvas // Draw a scale 0 element can cause all following draw wrong // And setTransform with scale 0 will cause set back transform failed. && !(m && !m[0] && !m[3]) // Ignore culled element && !(el.culling && isDisplayableCulled(el, this._width, this._height)) ) { var clipPaths = el.__clipPaths; var prevElClipPaths = scope.prevElClipPaths; // Optimize when clipping on group with several elements if (!prevElClipPaths || isClipPathChanged(clipPaths, prevElClipPaths)) { // If has previous clipping state, restore from it if (prevElClipPaths) { ctx.restore(); scope.prevElClipPaths = null; // Reset prevEl since context has been restored scope.prevEl = null; } // New clipping state if (clipPaths) { ctx.save(); doClip(clipPaths, ctx); scope.prevElClipPaths = clipPaths; } } el.beforeBrush && el.beforeBrush(ctx); el.brush(ctx, scope.prevEl || null); scope.prevEl = el; el.afterBrush && el.afterBrush(ctx); } }, /** * 获取 zlevel 所在层,如果不存在则会创建一个新的层 * @param {number} zlevel * @param {boolean} virtual Virtual layer will not be inserted into dom. * @return {module:zrender/Layer} */ getLayer: function (zlevel, virtual) { if (this._singleCanvas && !this._needsManuallyCompositing) { zlevel = CANVAS_ZLEVEL; } var layer = this._layers[zlevel]; if (!layer) { // Create a new layer layer = new Layer('zr_' + zlevel, this, this.dpr); layer.zlevel = zlevel; layer.__builtin__ = true; if (this._layerConfig[zlevel]) { merge(layer, this._layerConfig[zlevel], true); } // TODO Remove EL_AFTER_INCREMENTAL_INC magic number else if (this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC]) { merge(layer, this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC], true); } if (virtual) { layer.virtual = virtual; } this.insertLayer(zlevel, layer); // Context is created after dom inserted to document // Or excanvas will get 0px clientWidth and clientHeight layer.initContext(); } return layer; }, insertLayer: function (zlevel, layer) { var layersMap = this._layers; var zlevelList = this._zlevelList; var len = zlevelList.length; var prevLayer = null; var i = -1; var domRoot = this._domRoot; if (layersMap[zlevel]) { logError$1('ZLevel ' + zlevel + ' has been used already'); return; } // Check if is a valid layer if (!isLayerValid(layer)) { logError$1('Layer of zlevel ' + zlevel + ' is not valid'); return; } if (len > 0 && zlevel > zlevelList[0]) { for (i = 0; i < len - 1; i++) { if ( zlevelList[i] < zlevel && zlevelList[i + 1] > zlevel ) { break; } } prevLayer = layersMap[zlevelList[i]]; } zlevelList.splice(i + 1, 0, zlevel); layersMap[zlevel] = layer; // Vitual layer will not directly show on the screen. // (It can be a WebGL layer and assigned to a ZImage element) // But it still under management of zrender. if (!layer.virtual) { if (prevLayer) { var prevDom = prevLayer.dom; if (prevDom.nextSibling) { domRoot.insertBefore( layer.dom, prevDom.nextSibling ); } else { domRoot.appendChild(layer.dom); } } else { if (domRoot.firstChild) { domRoot.insertBefore(layer.dom, domRoot.firstChild); } else { domRoot.appendChild(layer.dom); } } } }, // Iterate each layer eachLayer: function (cb, context) { var zlevelList = this._zlevelList; var z; var i; for (i = 0; i < zlevelList.length; i++) { z = zlevelList[i]; cb.call(context, this._layers[z], z); } }, // Iterate each buildin layer eachBuiltinLayer: function (cb, context) { var zlevelList = this._zlevelList; var layer; var z; var i; for (i = 0; i < zlevelList.length; i++) { z = zlevelList[i]; layer = this._layers[z]; if (layer.__builtin__) { cb.call(context, layer, z); } } }, // Iterate each other layer except buildin layer eachOtherLayer: function (cb, context) { var zlevelList = this._zlevelList; var layer; var z; var i; for (i = 0; i < zlevelList.length; i++) { z = zlevelList[i]; layer = this._layers[z]; if (!layer.__builtin__) { cb.call(context, layer, z); } } }, /** * 获取所有已创建的层 * @param {Array.} [prevLayer] */ getLayers: function () { return this._layers; }, _updateLayerStatus: function (list) { this.eachBuiltinLayer(function (layer, z) { layer.__dirty = layer.__used = false; }); function updatePrevLayer(idx) { if (prevLayer) { if (prevLayer.__endIndex !== idx) { prevLayer.__dirty = true; } prevLayer.__endIndex = idx; } } if (this._singleCanvas) { for (var i = 1; i < list.length; i++) { var el = list[i]; if (el.zlevel !== list[i - 1].zlevel || el.incremental) { this._needsManuallyCompositing = true; break; } } } var prevLayer = null; var incrementalLayerCount = 0; var prevZlevel; for (var i = 0; i < list.length; i++) { var el = list[i]; var zlevel = el.zlevel; var layer; if (prevZlevel !== zlevel) { prevZlevel = zlevel; incrementalLayerCount = 0; } // TODO Not use magic number on zlevel. // Each layer with increment element can be separated to 3 layers. // (Other Element drawn after incremental element) // -----------------zlevel + EL_AFTER_INCREMENTAL_INC-------------------- // (Incremental element) // ----------------------zlevel + INCREMENTAL_INC------------------------ // (Element drawn before incremental element) // --------------------------------zlevel-------------------------------- if (el.incremental) { layer = this.getLayer(zlevel + INCREMENTAL_INC, this._needsManuallyCompositing); layer.incremental = true; incrementalLayerCount = 1; } else { layer = this.getLayer( zlevel + (incrementalLayerCount > 0 ? EL_AFTER_INCREMENTAL_INC : 0), this._needsManuallyCompositing ); } if (!layer.__builtin__) { logError$1('ZLevel ' + zlevel + ' has been used by unkown layer ' + layer.id); } if (layer !== prevLayer) { layer.__used = true; if (layer.__startIndex !== i) { layer.__dirty = true; } layer.__startIndex = i; if (!layer.incremental) { layer.__drawIndex = i; } else { // Mark layer draw index needs to update. layer.__drawIndex = -1; } updatePrevLayer(i); prevLayer = layer; } if (el.__dirty) { layer.__dirty = true; if (layer.incremental && layer.__drawIndex < 0) { // Start draw from the first dirty element. layer.__drawIndex = i; } } } updatePrevLayer(i); this.eachBuiltinLayer(function (layer, z) { // Used in last frame but not in this frame. Needs clear if (!layer.__used && layer.getElementCount() > 0) { layer.__dirty = true; layer.__startIndex = layer.__endIndex = layer.__drawIndex = 0; } // For incremental layer. In case start index changed and no elements are dirty. if (layer.__dirty && layer.__drawIndex < 0) { layer.__drawIndex = layer.__startIndex; } }); }, /** * 清除hover层外所有内容 */ clear: function () { this.eachBuiltinLayer(this._clearLayer); return this; }, _clearLayer: function (layer) { layer.clear(); }, setBackgroundColor: function (backgroundColor) { this._backgroundColor = backgroundColor; }, /** * 修改指定zlevel的绘制参数 * * @param {string} zlevel * @param {Object} config 配置对象 * @param {string} [config.clearColor=0] 每次清空画布的颜色 * @param {string} [config.motionBlur=false] 是否开启动态模糊 * @param {number} [config.lastFrameAlpha=0.7] * 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显 */ configLayer: function (zlevel, config) { if (config) { var layerConfig = this._layerConfig; if (!layerConfig[zlevel]) { layerConfig[zlevel] = config; } else { merge(layerConfig[zlevel], config, true); } for (var i = 0; i < this._zlevelList.length; i++) { var _zlevel = this._zlevelList[i]; // TODO Remove EL_AFTER_INCREMENTAL_INC magic number if (_zlevel === zlevel || _zlevel === zlevel + EL_AFTER_INCREMENTAL_INC) { var layer = this._layers[_zlevel]; merge(layer, layerConfig[zlevel], true); } } } }, /** * 删除指定层 * @param {number} zlevel 层所在的zlevel */ delLayer: function (zlevel) { var layers = this._layers; var zlevelList = this._zlevelList; var layer = layers[zlevel]; if (!layer) { return; } layer.dom.parentNode.removeChild(layer.dom); delete layers[zlevel]; zlevelList.splice(indexOf(zlevelList, zlevel), 1); }, /** * 区域大小变化后重绘 */ resize: function (width, height) { if (!this._domRoot.style) { // Maybe in node or worker if (width == null || height == null) { return; } this._width = width; this._height = height; this.getLayer(CANVAS_ZLEVEL).resize(width, height); } else { var domRoot = this._domRoot; // FIXME Why ? domRoot.style.display = 'none'; // Save input w/h var opts = this._opts; width != null && (opts.width = width); height != null && (opts.height = height); width = this._getSize(0); height = this._getSize(1); domRoot.style.display = ''; // 优化没有实际改变的resize if (this._width !== width || height !== this._height) { domRoot.style.width = width + 'px'; domRoot.style.height = height + 'px'; for (var id in this._layers) { if (this._layers.hasOwnProperty(id)) { this._layers[id].resize(width, height); } } each$1(this._progressiveLayers, function (layer) { layer.resize(width, height); }); this.refresh(true); } this._width = width; this._height = height; } return this; }, /** * 清除单独的一个层 * @param {number} zlevel */ clearLayer: function (zlevel) { var layer = this._layers[zlevel]; if (layer) { layer.clear(); } }, /** * 释放 */ dispose: function () { this.root.innerHTML = ''; this.root = this.storage = this._domRoot = this._layers = null; }, /** * Get canvas which has all thing rendered * @param {Object} opts * @param {string} [opts.backgroundColor] * @param {number} [opts.pixelRatio] */ getRenderedCanvas: function (opts) { opts = opts || {}; if (this._singleCanvas && !this._compositeManually) { return this._layers[CANVAS_ZLEVEL].dom; } var imageLayer = new Layer('image', this, opts.pixelRatio || this.dpr); imageLayer.initContext(); imageLayer.clear(false, opts.backgroundColor || this._backgroundColor); if (opts.pixelRatio <= this.dpr) { this.refresh(); var width = imageLayer.dom.width; var height = imageLayer.dom.height; var ctx = imageLayer.ctx; this.eachLayer(function (layer) { if (layer.__builtin__) { ctx.drawImage(layer.dom, 0, 0, width, height); } else if (layer.renderToCanvas) { imageLayer.ctx.save(); layer.renderToCanvas(imageLayer.ctx); imageLayer.ctx.restore(); } }); } else { // PENDING, echarts-gl and incremental rendering. var scope = {}; var displayList = this.storage.getDisplayList(true); for (var i = 0; i < displayList.length; i++) { var el = displayList[i]; this._doPaintEl(el, imageLayer, true, scope); } } return imageLayer.dom; }, /** * 获取绘图区域宽度 */ getWidth: function () { return this._width; }, /** * 获取绘图区域高度 */ getHeight: function () { return this._height; }, _getSize: function (whIdx) { var opts = this._opts; var wh = ['width', 'height'][whIdx]; var cwh = ['clientWidth', 'clientHeight'][whIdx]; var plt = ['paddingLeft', 'paddingTop'][whIdx]; var prb = ['paddingRight', 'paddingBottom'][whIdx]; if (opts[wh] != null && opts[wh] !== 'auto') { return parseFloat(opts[wh]); } var root = this.root; // IE8 does not support getComputedStyle, but it use VML. var stl = document.defaultView.getComputedStyle(root); return ( (root[cwh] || parseInt10(stl[wh]) || parseInt10(root.style[wh])) - (parseInt10(stl[plt]) || 0) - (parseInt10(stl[prb]) || 0) ) | 0; }, pathToImage: function (path, dpr) { dpr = dpr || this.dpr; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var rect = path.getBoundingRect(); var style = path.style; var shadowBlurSize = style.shadowBlur * dpr; var shadowOffsetX = style.shadowOffsetX * dpr; var shadowOffsetY = style.shadowOffsetY * dpr; var lineWidth = style.hasStroke() ? style.lineWidth : 0; var leftMargin = Math.max(lineWidth / 2, -shadowOffsetX + shadowBlurSize); var rightMargin = Math.max(lineWidth / 2, shadowOffsetX + shadowBlurSize); var topMargin = Math.max(lineWidth / 2, -shadowOffsetY + shadowBlurSize); var bottomMargin = Math.max(lineWidth / 2, shadowOffsetY + shadowBlurSize); var width = rect.width + leftMargin + rightMargin; var height = rect.height + topMargin + bottomMargin; canvas.width = width * dpr; canvas.height = height * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, width, height); ctx.dpr = dpr; var pathTransform = { position: path.position, rotation: path.rotation, scale: path.scale }; path.position = [leftMargin - rect.x, topMargin - rect.y]; path.rotation = 0; path.scale = [1, 1]; path.updateTransform(); if (path) { path.brush(ctx); } var ImageShape = ZImage; var imgShape = new ImageShape({ style: { x: 0, y: 0, image: canvas } }); if (pathTransform.position != null) { imgShape.position = path.position = pathTransform.position; } if (pathTransform.rotation != null) { imgShape.rotation = path.rotation = pathTransform.rotation; } if (pathTransform.scale != null) { imgShape.scale = path.scale = pathTransform.scale; } return imgShape; } }; /** * Animation main class, dispatch and manage all animation controllers * * @module zrender/animation/Animation * @author pissang(https://github.com/pissang) */ // TODO Additive animation // http://iosoteric.com/additive-animations-animatewithduration-in-ios-8/ // https://developer.apple.com/videos/wwdc2014/#236 /** * @typedef {Object} IZRenderStage * @property {Function} update */ /** * @alias module:zrender/animation/Animation * @constructor * @param {Object} [options] * @param {Function} [options.onframe] * @param {IZRenderStage} [options.stage] * @example * var animation = new Animation(); * var obj = { * x: 100, * y: 100 * }; * animation.animate(node.position) * .when(1000, { * x: 500, * y: 500 * }) * .when(2000, { * x: 100, * y: 100 * }) * .start('spline'); */ var Animation = function (options) { options = options || {}; this.stage = options.stage || {}; this.onframe = options.onframe || function () {}; // private properties this._clips = []; this._running = false; this._time; this._pausedTime; this._pauseStart; this._paused = false; Eventful.call(this); }; Animation.prototype = { constructor: Animation, /** * Add clip * @param {module:zrender/animation/Clip} clip */ addClip: function (clip) { this._clips.push(clip); }, /** * Add animator * @param {module:zrender/animation/Animator} animator */ addAnimator: function (animator) { animator.animation = this; var clips = animator.getClips(); for (var i = 0; i < clips.length; i++) { this.addClip(clips[i]); } }, /** * Delete animation clip * @param {module:zrender/animation/Clip} clip */ removeClip: function (clip) { var idx = indexOf(this._clips, clip); if (idx >= 0) { this._clips.splice(idx, 1); } }, /** * Delete animation clip * @param {module:zrender/animation/Animator} animator */ removeAnimator: function (animator) { var clips = animator.getClips(); for (var i = 0; i < clips.length; i++) { this.removeClip(clips[i]); } animator.animation = null; }, _update: function () { var time = new Date().getTime() - this._pausedTime; var delta = time - this._time; var clips = this._clips; var len = clips.length; var deferredEvents = []; var deferredClips = []; for (var i = 0; i < len; i++) { var clip = clips[i]; var e = clip.step(time, delta); // Throw out the events need to be called after // stage.update, like destroy if (e) { deferredEvents.push(e); deferredClips.push(clip); } } // Remove the finished clip for (var i = 0; i < len;) { if (clips[i]._needsRemove) { clips[i] = clips[len - 1]; clips.pop(); len--; } else { i++; } } len = deferredEvents.length; for (var i = 0; i < len; i++) { deferredClips[i].fire(deferredEvents[i]); } this._time = time; this.onframe(delta); // 'frame' should be triggered before stage, because upper application // depends on the sequence (e.g., echarts-stream and finish // event judge) this.trigger('frame', delta); if (this.stage.update) { this.stage.update(); } }, _startLoop: function () { var self = this; this._running = true; function step() { if (self._running) { requestAnimationFrame(step); !self._paused && self._update(); } } requestAnimationFrame(step); }, /** * Start animation. */ start: function () { this._time = new Date().getTime(); this._pausedTime = 0; this._startLoop(); }, /** * Stop animation. */ stop: function () { this._running = false; }, /** * Pause animation. */ pause: function () { if (!this._paused) { this._pauseStart = new Date().getTime(); this._paused = true; } }, /** * Resume animation. */ resume: function () { if (this._paused) { this._pausedTime += (new Date().getTime()) - this._pauseStart; this._paused = false; } }, /** * Clear animation. */ clear: function () { this._clips = []; }, /** * Whether animation finished. */ isFinished: function () { return !this._clips.length; }, /** * Creat animator for a target, whose props can be animated. * * @param {Object} target * @param {Object} options * @param {boolean} [options.loop=false] Whether loop animation. * @param {Function} [options.getter=null] Get value from target. * @param {Function} [options.setter=null] Set value to target. * @return {module:zrender/animation/Animation~Animator} */ // TODO Gap animate: function (target, options) { options = options || {}; var animator = new Animator( target, options.loop, options.getter, options.setter ); this.addAnimator(animator); return animator; } }; mixin(Animation, Eventful); /* global document */ var TOUCH_CLICK_DELAY = 300; var globalEventSupported = env$1.domSupported; var localNativeListenerNames = (function () { var mouseHandlerNames = [ 'click', 'dblclick', 'mousewheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu' ]; var touchHandlerNames = [ 'touchstart', 'touchend', 'touchmove' ]; var pointerEventNameMap = { pointerdown: 1, pointerup: 1, pointermove: 1, pointerout: 1 }; var pointerHandlerNames = map(mouseHandlerNames, function (name) { var nm = name.replace('mouse', 'pointer'); return pointerEventNameMap.hasOwnProperty(nm) ? nm : name; }); return { mouse: mouseHandlerNames, touch: touchHandlerNames, pointer: pointerHandlerNames }; })(); var globalNativeListenerNames = { mouse: ['mousemove', 'mouseup'], pointer: ['pointermove', 'pointerup'] }; function eventNameFix(name) { return (name === 'mousewheel' && env$1.browser.firefox) ? 'DOMMouseScroll' : name; } function isPointerFromTouch(event) { var pointerType = event.pointerType; return pointerType === 'pen' || pointerType === 'touch'; } // function useMSGuesture(handlerProxy, event) { // return isPointerFromTouch(event) && !!handlerProxy._msGesture; // } // function onMSGestureChange(proxy, event) { // if (event.translationX || event.translationY) { // // mousemove is carried by MSGesture to reduce the sensitivity. // proxy.handler.dispatchToElement(event.target, 'mousemove', event); // } // if (event.scale !== 1) { // event.pinchX = event.offsetX; // event.pinchY = event.offsetY; // event.pinchScale = event.scale; // proxy.handler.dispatchToElement(event.target, 'pinch', event); // } // } /** * Prevent mouse event from being dispatched after Touch Events action * @see * 1. Mobile browsers dispatch mouse events 300ms after touchend. * 2. Chrome for Android dispatch mousedown for long-touch about 650ms * Result: Blocking Mouse Events for 700ms. * * @param {DOMHandlerScope} scope */ function setTouchTimer(scope) { scope.touching = true; if (scope.touchTimer != null) { clearTimeout(scope.touchTimer); scope.touchTimer = null; } scope.touchTimer = setTimeout(function () { scope.touching = false; scope.touchTimer = null; }, 700); } // Mark touch, which is useful in distinguish touch and // mouse event in upper applicatoin. function markTouch(event) { event && (event.zrByTouch = true); } // function markTriggeredFromLocal(event) { // event && (event.__zrIsFromLocal = true); // } // function isTriggeredFromLocal(instance, event) { // return !!(event && event.__zrIsFromLocal); // } function normalizeGlobalEvent(instance, event) { // offsetX, offsetY still need to be calculated. They are necessary in the event // handlers of the upper applications. Set `true` to force calculate them. return normalizeEvent(instance.dom, new FakeGlobalEvent(instance, event), true); } /** * Detect whether the given el is in `painterRoot`. */ function isLocalEl(instance, el) { var elTmp = el; var isLocal = false; while (elTmp && elTmp.nodeType !== 9 && !( isLocal = elTmp.domBelongToZr || (elTmp !== el && elTmp === instance.painterRoot) ) ) { elTmp = elTmp.parentNode; } return isLocal; } /** * Make a fake event but not change the original event, * becuase the global event probably be used by other * listeners not belonging to zrender. * @class */ function FakeGlobalEvent(instance, event) { this.type = event.type; this.target = this.currentTarget = instance.dom; this.pointerType = event.pointerType; // Necessray for the force calculation of zrX, zrY this.clientX = event.clientX; this.clientY = event.clientY; // Because we do not mount global listeners to touch events, // we do not copy `targetTouches` and `changedTouches` here. } var fakeGlobalEventProto = FakeGlobalEvent.prototype; // we make the default methods on the event do nothing, // otherwise it is dangerous. See more details in // [Drag outside] in `Handler.js`. fakeGlobalEventProto.stopPropagation = fakeGlobalEventProto.stopImmediatePropagation = fakeGlobalEventProto.preventDefault = noop; /** * Local DOM Handlers * @this {HandlerProxy} */ var localDOMHandlers = { mousedown: function (event) { event = normalizeEvent(this.dom, event); this._mayPointerCapture = [event.zrX, event.zrY]; this.trigger('mousedown', event); }, mousemove: function (event) { event = normalizeEvent(this.dom, event); var downPoint = this._mayPointerCapture; if (downPoint && (event.zrX !== downPoint[0] || event.zrY !== downPoint[1])) { togglePointerCapture(this, true); } this.trigger('mousemove', event); }, mouseup: function (event) { event = normalizeEvent(this.dom, event); togglePointerCapture(this, false); this.trigger('mouseup', event); }, mouseout: function (event) { event = normalizeEvent(this.dom, event); // Similarly to the browser did on `document` and touch event, // `globalout` will be delayed to final pointer cature release. if (this._pointerCapturing) { event.zrEventControl = 'no_globalout'; } // There might be some doms created by upper layer application // at the same level of painter.getViewportRoot() (e.g., tooltip // dom created by echarts), where 'globalout' event should not // be triggered when mouse enters these doms. (But 'mouseout' // should be triggered at the original hovered element as usual). var element = event.toElement || event.relatedTarget; event.zrIsToLocalDOM = isLocalEl(this, element); this.trigger('mouseout', event); }, touchstart: function (event) { // Default mouse behaviour should not be disabled here. // For example, page may needs to be slided. event = normalizeEvent(this.dom, event); markTouch(event); this._lastTouchMoment = new Date(); this.handler.processGesture(event, 'start'); // For consistent event listener for both touch device and mouse device, // we simulate "mouseover-->mousedown" in touch device. So we trigger // `mousemove` here (to trigger `mouseover` inside), and then trigger // `mousedown`. localDOMHandlers.mousemove.call(this, event); localDOMHandlers.mousedown.call(this, event); }, touchmove: function (event) { event = normalizeEvent(this.dom, event); markTouch(event); this.handler.processGesture(event, 'change'); // Mouse move should always be triggered no matter whether // there is gestrue event, because mouse move and pinch may // be used at the same time. localDOMHandlers.mousemove.call(this, event); }, touchend: function (event) { event = normalizeEvent(this.dom, event); markTouch(event); this.handler.processGesture(event, 'end'); localDOMHandlers.mouseup.call(this, event); // Do not trigger `mouseout` here, in spite of `mousemove`(`mouseover`) is // triggered in `touchstart`. This seems to be illogical, but by this mechanism, // we can conveniently implement "hover style" in both PC and touch device just // by listening to `mouseover` to add "hover style" and listening to `mouseout` // to remove "hover style" on an element, without any additional code for // compatibility. (`mouseout` will not be triggered in `touchend`, so "hover // style" will remain for user view) // click event should always be triggered no matter whether // there is gestrue event. System click can not be prevented. if (+new Date() - this._lastTouchMoment < TOUCH_CLICK_DELAY) { localDOMHandlers.click.call(this, event); } }, pointerdown: function (event) { localDOMHandlers.mousedown.call(this, event); // if (useMSGuesture(this, event)) { // this._msGesture.addPointer(event.pointerId); // } }, pointermove: function (event) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we dont support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { localDOMHandlers.mousemove.call(this, event); } }, pointerup: function (event) { localDOMHandlers.mouseup.call(this, event); }, pointerout: function (event) { // pointerout will be triggered when tap on touch screen // (IE11+/Edge on MS Surface) after click event triggered, // which is inconsistent with the mousout behavior we defined // in touchend. So we unify them. // (check localDOMHandlers.touchend for detailed explanation) if (!isPointerFromTouch(event)) { localDOMHandlers.mouseout.call(this, event); } } }; /** * Othere DOM UI Event handlers for zr dom. * @this {HandlerProxy} */ each$1(['click', 'mousewheel', 'dblclick', 'contextmenu'], function (name) { localDOMHandlers[name] = function (event) { event = normalizeEvent(this.dom, event); this.trigger(name, event); }; }); /** * DOM UI Event handlers for global page. * * [Caution]: * those handlers should both support in capture phase and bubble phase! * * @this {HandlerProxy} */ var globalDOMHandlers = { pointermove: function (event) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we dont support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { globalDOMHandlers.mousemove.call(this, event); } }, pointerup: function (event) { globalDOMHandlers.mouseup.call(this, event); }, mousemove: function (event) { this.trigger('mousemove', event); }, mouseup: function (event) { var pointerCaptureReleasing = this._pointerCapturing; togglePointerCapture(this, false); this.trigger('mouseup', event); if (pointerCaptureReleasing) { event.zrEventControl = 'only_globalout'; this.trigger('mouseout', event); } } }; /** * @param {HandlerProxy} instance * @param {DOMHandlerScope} scope */ function mountLocalDOMEventListeners(instance, scope) { var domHandlers = scope.domHandlers; if (env$1.pointerEventsSupported) { // Only IE11+/Edge // 1. On devices that both enable touch and mouse (e.g., MS Surface and lenovo X240), // IE11+/Edge do not trigger touch event, but trigger pointer event and mouse event // at the same time. // 2. On MS Surface, it probablely only trigger mousedown but no mouseup when tap on // screen, which do not occurs in pointer event. // So we use pointer event to both detect touch gesture and mouse behavior. each$1(localNativeListenerNames.pointer, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); }); }); // FIXME // Note: MS Gesture require CSS touch-action set. But touch-action is not reliable, // which does not prevent defuault behavior occasionally (which may cause view port // zoomed in but use can not zoom it back). And event.preventDefault() does not work. // So we have to not to use MSGesture and not to support touchmove and pinch on MS // touch screen. And we only support click behavior on MS touch screen now. // MS Gesture Event is only supported on IE11+/Edge and on Windows 8+. // We dont support touch on IE on win7. // See // if (typeof MSGesture === 'function') { // (this._msGesture = new MSGesture()).target = dom; // jshint ignore:line // dom.addEventListener('MSGestureChange', onMSGestureChange); // } } else { if (env$1.touchEventsSupported) { each$1(localNativeListenerNames.touch, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); setTouchTimer(scope); }); }); // Handler of 'mouseout' event is needed in touch mode, which will be mounted below. // addEventListener(root, 'mouseout', this._mouseoutHandler); } // 1. Considering some devices that both enable touch and mouse event (like on MS Surface // and lenovo X240, @see #2350), we make mouse event be always listened, otherwise // mouse event can not be handle in those devices. // 2. On MS Surface, Chrome will trigger both touch event and mouse event. How to prevent // mouseevent after touch event triggered, see `setTouchTimer`. each$1(localNativeListenerNames.mouse, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event) { event = getNativeEvent(event); if (!scope.touching) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); } }); }); } } /** * @param {HandlerProxy} instance * @param {DOMHandlerScope} scope */ function mountGlobalDOMEventListeners(instance, scope) { // Only IE11+/Edge. See the comment in `mountLocalDOMEventListeners`. if (env$1.pointerEventsSupported) { each$1(globalNativeListenerNames.pointer, mount); } // Touch event has implemented "drag outside" so we do not mount global listener for touch event. // (see https://www.w3.org/TR/touch-events/#the-touchmove-event) // We do not consider "both-support-touch-and-mouse device" for this feature (see the comment of // `mountLocalDOMEventListeners`) to avoid bugs util some requirements come. else if (!env$1.touchEventsSupported) { each$1(globalNativeListenerNames.mouse, mount); } function mount(nativeEventName) { function nativeEventListener(event) { event = getNativeEvent(event); // See the reason in [Drag outside] in `Handler.js` // This checking supports both `useCapture` or not. // PENDING: if there is performance issue in some devices, // we probably can not use `useCapture` and change a easier // to judes whether local (mark). if (!isLocalEl(instance, event.target)) { event = normalizeGlobalEvent(instance, event); scope.domHandlers[nativeEventName].call(instance, event); } } mountSingleDOMEventListener( scope, nativeEventName, nativeEventListener, {capture: true} // See [Drag Outside] in `Handler.js` ); } } function mountSingleDOMEventListener(scope, nativeEventName, listener, opt) { scope.mounted[nativeEventName] = listener; scope.listenerOpts[nativeEventName] = opt; addEventListener(scope.domTarget, eventNameFix(nativeEventName), listener, opt); } function unmountDOMEventListeners(scope) { var mounted = scope.mounted; for (var nativeEventName in mounted) { if (mounted.hasOwnProperty(nativeEventName)) { removeEventListener( scope.domTarget, eventNameFix(nativeEventName), mounted[nativeEventName], scope.listenerOpts[nativeEventName] ); } } scope.mounted = {}; } /** * See [Drag Outside] in `Handler.js`. * @implement * @param {boolean} isPointerCapturing Should never be `null`/`undefined`. * `true`: start to capture pointer if it is not capturing. * `false`: end the capture if it is capturing. */ function togglePointerCapture(instance, isPointerCapturing) { instance._mayPointerCapture = null; if (globalEventSupported && (instance._pointerCapturing ^ isPointerCapturing)) { instance._pointerCapturing = isPointerCapturing; var globalHandlerScope = instance._globalHandlerScope; isPointerCapturing ? mountGlobalDOMEventListeners(instance, globalHandlerScope) : unmountDOMEventListeners(globalHandlerScope); } } /** * @inner * @class */ function DOMHandlerScope(domTarget, domHandlers) { this.domTarget = domTarget; this.domHandlers = domHandlers; // Key: eventName, value: mounted handler funcitons. // Used for unmount. this.mounted = {}; this.listenerOpts = {}; this.touchTimer = null; this.touching = false; } /** * @public * @class */ function HandlerDomProxy(dom, painterRoot) { Eventful.call(this); this.dom = dom; this.painterRoot = painterRoot; this._localHandlerScope = new DOMHandlerScope(dom, localDOMHandlers); if (globalEventSupported) { this._globalHandlerScope = new DOMHandlerScope(document, globalDOMHandlers); } /** * @type {boolean} */ this._pointerCapturing = false; /** * @type {Array.} [x, y] or null. */ this._mayPointerCapture = null; mountLocalDOMEventListeners(this, this._localHandlerScope); } var handlerDomProxyProto = HandlerDomProxy.prototype; handlerDomProxyProto.dispose = function () { unmountDOMEventListeners(this._localHandlerScope); if (globalEventSupported) { unmountDOMEventListeners(this._globalHandlerScope); } }; handlerDomProxyProto.setCursor = function (cursorStyle) { this.dom.style && (this.dom.style.cursor = cursorStyle || 'default'); }; mixin(HandlerDomProxy, Eventful); /*! * ZRender, a high performance 2d drawing library. * * Copyright (c) 2013, Baidu Inc. * All rights reserved. * * LICENSE * https://github.com/ecomfe/zrender/blob/master/LICENSE.txt */ var useVML = !env$1.canvasSupported; var painterCtors = { canvas: Painter }; var instances$1 = {}; // ZRender实例map索引 /** * @type {string} */ var version$1 = '4.3.2'; /** * Initializing a zrender instance * @param {HTMLElement} dom * @param {Object} [opts] * @param {string} [opts.renderer='canvas'] 'canvas' or 'svg' * @param {number} [opts.devicePixelRatio] * @param {number|string} [opts.width] Can be 'auto' (the same as null/undefined) * @param {number|string} [opts.height] Can be 'auto' (the same as null/undefined) * @return {module:zrender/ZRender} */ function init$1(dom, opts) { var zr = new ZRender(guid(), dom, opts); instances$1[zr.id] = zr; return zr; } /** * Dispose zrender instance * @param {module:zrender/ZRender} zr */ function dispose$1(zr) { if (zr) { zr.dispose(); } else { for (var key in instances$1) { if (instances$1.hasOwnProperty(key)) { instances$1[key].dispose(); } } instances$1 = {}; } return this; } /** * Get zrender instance by id * @param {string} id zrender instance id * @return {module:zrender/ZRender} */ function getInstance(id) { return instances$1[id]; } function registerPainter(name, Ctor) { painterCtors[name] = Ctor; } function delInstance(id) { delete instances$1[id]; } /** * @module zrender/ZRender */ /** * @constructor * @alias module:zrender/ZRender * @param {string} id * @param {HTMLElement} dom * @param {Object} opts * @param {string} [opts.renderer='canvas'] 'canvas' or 'svg' * @param {number} [opts.devicePixelRatio] * @param {number} [opts.width] Can be 'auto' (the same as null/undefined) * @param {number} [opts.height] Can be 'auto' (the same as null/undefined) */ var ZRender = function (id, dom, opts) { opts = opts || {}; /** * @type {HTMLDomElement} */ this.dom = dom; /** * @type {string} */ this.id = id; var self = this; var storage = new Storage(); var rendererType = opts.renderer; // TODO WebGL if (useVML) { if (!painterCtors.vml) { throw new Error('You need to require \'zrender/vml/vml\' to support IE8'); } rendererType = 'vml'; } else if (!rendererType || !painterCtors[rendererType]) { rendererType = 'canvas'; } var painter = new painterCtors[rendererType](dom, storage, opts, id); this.storage = storage; this.painter = painter; var handerProxy = (!env$1.node && !env$1.worker) ? new HandlerDomProxy(painter.getViewportRoot(), painter.root) : null; this.handler = new Handler(storage, painter, handerProxy, painter.root); /** * @type {module:zrender/animation/Animation} */ this.animation = new Animation({ stage: { update: bind(this.flush, this) } }); this.animation.start(); /** * @type {boolean} * @private */ this._needsRefresh; // 修改 storage.delFromStorage, 每次删除元素之前删除动画 // FIXME 有点ugly var oldDelFromStorage = storage.delFromStorage; var oldAddToStorage = storage.addToStorage; storage.delFromStorage = function (el) { oldDelFromStorage.call(storage, el); el && el.removeSelfFromZr(self); }; storage.addToStorage = function (el) { oldAddToStorage.call(storage, el); el.addSelfToZr(self); }; }; ZRender.prototype = { constructor: ZRender, /** * 获取实例唯一标识 * @return {string} */ getId: function () { return this.id; }, /** * 添加元素 * @param {module:zrender/Element} el */ add: function (el) { this.storage.addRoot(el); this._needsRefresh = true; }, /** * 删除元素 * @param {module:zrender/Element} el */ remove: function (el) { this.storage.delRoot(el); this._needsRefresh = true; }, /** * Change configuration of layer * @param {string} zLevel * @param {Object} config * @param {string} [config.clearColor=0] Clear color * @param {string} [config.motionBlur=false] If enable motion blur * @param {number} [config.lastFrameAlpha=0.7] Motion blur factor. Larger value cause longer trailer */ configLayer: function (zLevel, config) { if (this.painter.configLayer) { this.painter.configLayer(zLevel, config); } this._needsRefresh = true; }, /** * Set background color * @param {string} backgroundColor */ setBackgroundColor: function (backgroundColor) { if (this.painter.setBackgroundColor) { this.painter.setBackgroundColor(backgroundColor); } this._needsRefresh = true; }, /** * Repaint the canvas immediately */ refreshImmediately: function () { // var start = new Date(); // Clear needsRefresh ahead to avoid something wrong happens in refresh // Or it will cause zrender refreshes again and again. this._needsRefresh = this._needsRefreshHover = false; this.painter.refresh(); // Avoid trigger zr.refresh in Element#beforeUpdate hook this._needsRefresh = this._needsRefreshHover = false; // var end = new Date(); // var log = document.getElementById('log'); // if (log) { // log.innerHTML = log.innerHTML + '
' + (end - start); // } }, /** * Mark and repaint the canvas in the next frame of browser */ refresh: function () { this._needsRefresh = true; }, /** * Perform all refresh */ flush: function () { var triggerRendered; if (this._needsRefresh) { triggerRendered = true; this.refreshImmediately(); } if (this._needsRefreshHover) { triggerRendered = true; this.refreshHoverImmediately(); } triggerRendered && this.trigger('rendered'); }, /** * Add element to hover layer * @param {module:zrender/Element} el * @param {Object} style */ addHover: function (el, style) { if (this.painter.addHover) { var elMirror = this.painter.addHover(el, style); this.refreshHover(); return elMirror; } }, /** * Add element from hover layer * @param {module:zrender/Element} el */ removeHover: function (el) { if (this.painter.removeHover) { this.painter.removeHover(el); this.refreshHover(); } }, /** * Clear all hover elements in hover layer * @param {module:zrender/Element} el */ clearHover: function () { if (this.painter.clearHover) { this.painter.clearHover(); this.refreshHover(); } }, /** * Refresh hover in next frame */ refreshHover: function () { this._needsRefreshHover = true; }, /** * Refresh hover immediately */ refreshHoverImmediately: function () { this._needsRefreshHover = false; this.painter.refreshHover && this.painter.refreshHover(); }, /** * Resize the canvas. * Should be invoked when container size is changed * @param {Object} [opts] * @param {number|string} [opts.width] Can be 'auto' (the same as null/undefined) * @param {number|string} [opts.height] Can be 'auto' (the same as null/undefined) */ resize: function (opts) { opts = opts || {}; this.painter.resize(opts.width, opts.height); this.handler.resize(); }, /** * Stop and clear all animation immediately */ clearAnimation: function () { this.animation.clear(); }, /** * Get container width */ getWidth: function () { return this.painter.getWidth(); }, /** * Get container height */ getHeight: function () { return this.painter.getHeight(); }, /** * Export the canvas as Base64 URL * @param {string} type * @param {string} [backgroundColor='#fff'] * @return {string} Base64 URL */ // toDataURL: function(type, backgroundColor) { // return this.painter.getRenderedCanvas({ // backgroundColor: backgroundColor // }).toDataURL(type); // }, /** * Converting a path to image. * It has much better performance of drawing image rather than drawing a vector path. * @param {module:zrender/graphic/Path} e * @param {number} width * @param {number} height */ pathToImage: function (e, dpr) { return this.painter.pathToImage(e, dpr); }, /** * Set default cursor * @param {string} [cursorStyle='default'] 例如 crosshair */ setCursorStyle: function (cursorStyle) { this.handler.setCursorStyle(cursorStyle); }, /** * Find hovered element * @param {number} x * @param {number} y * @return {Object} {target, topTarget} */ findHover: function (x, y) { return this.handler.findHover(x, y); }, /** * Bind event * * @param {string} eventName Event name * @param {Function} eventHandler Handler function * @param {Object} [context] Context object */ on: function (eventName, eventHandler, context) { this.handler.on(eventName, eventHandler, context); }, /** * Unbind event * @param {string} eventName Event name * @param {Function} [eventHandler] Handler function */ off: function (eventName, eventHandler) { this.handler.off(eventName, eventHandler); }, /** * Trigger event manually * * @param {string} eventName Event name * @param {event=} event Event object */ trigger: function (eventName, event) { this.handler.trigger(eventName, event); }, /** * Clear all objects and the canvas. */ clear: function () { this.storage.delRoot(); this.painter.clear(); }, /** * Dispose self. */ dispose: function () { this.animation.stop(); this.clear(); this.storage.dispose(); this.painter.dispose(); this.handler.dispose(); this.animation = this.storage = this.painter = this.handler = null; delInstance(this.id); } }; var zrender = (Object.freeze || Object)({ version: version$1, init: init$1, dispose: dispose$1, getInstance: getInstance, registerPainter: registerPainter }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$2 = each$1; var isObject$2 = isObject$1; var isArray$1 = isArray; /** * Make the name displayable. But we should * make sure it is not duplicated with user * specified name, so use '\0'; */ var DUMMY_COMPONENT_NAME_PREFIX = 'series\0'; /** * If value is not array, then translate it to array. * @param {*} value * @return {Array} [value] or value */ function normalizeToArray(value) { return value instanceof Array ? value : value == null ? [] : [value]; } /** * Sync default option between normal and emphasis like `position` and `show` * In case some one will write code like * label: { * show: false, * position: 'outside', * fontSize: 18 * }, * emphasis: { * label: { show: true } * } * @param {Object} opt * @param {string} key * @param {Array.} subOpts */ function defaultEmphasis(opt, key, subOpts) { // Caution: performance sensitive. if (opt) { opt[key] = opt[key] || {}; opt.emphasis = opt.emphasis || {}; opt.emphasis[key] = opt.emphasis[key] || {}; // Default emphasis option from normal for (var i = 0, len = subOpts.length; i < len; i++) { var subOptName = subOpts[i]; if (!opt.emphasis[key].hasOwnProperty(subOptName) && opt[key].hasOwnProperty(subOptName) ) { opt.emphasis[key][subOptName] = opt[key][subOptName]; } } } } var TEXT_STYLE_OPTIONS = [ 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'rich', 'tag', 'color', 'textBorderColor', 'textBorderWidth', 'width', 'height', 'lineHeight', 'align', 'verticalAlign', 'baseline', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', 'textShadowColor', 'textShadowBlur', 'textShadowOffsetX', 'textShadowOffsetY', 'backgroundColor', 'borderColor', 'borderWidth', 'borderRadius', 'padding' ]; // modelUtil.LABEL_OPTIONS = modelUtil.TEXT_STYLE_OPTIONS.concat([ // 'position', 'offset', 'rotate', 'origin', 'show', 'distance', 'formatter', // 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', // // FIXME: deprecated, check and remove it. // 'textStyle' // ]); /** * The method do not ensure performance. * data could be [12, 2323, {value: 223}, [1221, 23], {value: [2, 23]}] * This helper method retieves value from data. * @param {string|number|Date|Array|Object} dataItem * @return {number|string|Date|Array.} */ function getDataItemValue(dataItem) { return (isObject$2(dataItem) && !isArray$1(dataItem) && !(dataItem instanceof Date)) ? dataItem.value : dataItem; } /** * data could be [12, 2323, {value: 223}, [1221, 23], {value: [2, 23]}] * This helper method determine if dataItem has extra option besides value * @param {string|number|Date|Array|Object} dataItem */ function isDataItemOption(dataItem) { return isObject$2(dataItem) && !(dataItem instanceof Array); // // markLine data can be array // && !(dataItem[0] && isObject(dataItem[0]) && !(dataItem[0] instanceof Array)); } /** * Mapping to exists for merge. * * @public * @param {Array.|Array.} exists * @param {Object|Array.} newCptOptions * @return {Array.} Result, like [{exist: ..., option: ...}, {}], * index of which is the same as exists. */ function mappingToExists(exists, newCptOptions) { // Mapping by the order by original option (but not order of // new option) in merge mode. Because we should ensure // some specified index (like xAxisIndex) is consistent with // original option, which is easy to understand, espatially in // media query. And in most case, merge option is used to // update partial option but not be expected to change order. newCptOptions = (newCptOptions || []).slice(); var result = map(exists || [], function (obj, index) { return {exist: obj}; }); // Mapping by id or name if specified. each$2(newCptOptions, function (cptOption, index) { if (!isObject$2(cptOption)) { return; } // id has highest priority. for (var i = 0; i < result.length; i++) { if (!result[i].option // Consider name: two map to one. && cptOption.id != null && result[i].exist.id === cptOption.id + '' ) { result[i].option = cptOption; newCptOptions[index] = null; return; } } for (var i = 0; i < result.length; i++) { var exist = result[i].exist; if (!result[i].option // Consider name: two map to one. // Can not match when both ids exist but different. && (exist.id == null || cptOption.id == null) && cptOption.name != null && !isIdInner(cptOption) && !isIdInner(exist) && exist.name === cptOption.name + '' ) { result[i].option = cptOption; newCptOptions[index] = null; return; } } }); // Otherwise mapping by index. each$2(newCptOptions, function (cptOption, index) { if (!isObject$2(cptOption)) { return; } var i = 0; for (; i < result.length; i++) { var exist = result[i].exist; if (!result[i].option // Existing model that already has id should be able to // mapped to (because after mapping performed model may // be assigned with a id, whish should not affect next // mapping), except those has inner id. && !isIdInner(exist) // Caution: // Do not overwrite id. But name can be overwritten, // because axis use name as 'show label text'. // 'exist' always has id and name and we dont // need to check it. && cptOption.id == null ) { result[i].option = cptOption; break; } } if (i >= result.length) { result.push({option: cptOption}); } }); return result; } /** * Make id and name for mapping result (result of mappingToExists) * into `keyInfo` field. * * @public * @param {Array.} Result, like [{exist: ..., option: ...}, {}], * which order is the same as exists. * @return {Array.} The input. */ function makeIdAndName(mapResult) { // We use this id to hash component models and view instances // in echarts. id can be specified by user, or auto generated. // The id generation rule ensures new view instance are able // to mapped to old instance when setOption are called in // no-merge mode. So we generate model id by name and plus // type in view id. // name can be duplicated among components, which is convenient // to specify multi components (like series) by one name. // Ensure that each id is distinct. var idMap = createHashMap(); each$2(mapResult, function (item, index) { var existCpt = item.exist; existCpt && idMap.set(existCpt.id, item); }); each$2(mapResult, function (item, index) { var opt = item.option; assert$1( !opt || opt.id == null || !idMap.get(opt.id) || idMap.get(opt.id) === item, 'id duplicates: ' + (opt && opt.id) ); opt && opt.id != null && idMap.set(opt.id, item); !item.keyInfo && (item.keyInfo = {}); }); // Make name and id. each$2(mapResult, function (item, index) { var existCpt = item.exist; var opt = item.option; var keyInfo = item.keyInfo; if (!isObject$2(opt)) { return; } // name can be overwitten. Consider case: axis.name = '20km'. // But id generated by name will not be changed, which affect // only in that case: setOption with 'not merge mode' and view // instance will be recreated, which can be accepted. keyInfo.name = opt.name != null ? opt.name + '' : existCpt ? existCpt.name // Avoid diffferent series has the same name, // because name may be used like in color pallet. : DUMMY_COMPONENT_NAME_PREFIX + index; if (existCpt) { keyInfo.id = existCpt.id; } else if (opt.id != null) { keyInfo.id = opt.id + ''; } else { // Consider this situatoin: // optionA: [{name: 'a'}, {name: 'a'}, {..}] // optionB [{..}, {name: 'a'}, {name: 'a'}] // Series with the same name between optionA and optionB // should be mapped. var idNum = 0; do { keyInfo.id = '\0' + keyInfo.name + '\0' + idNum++; } while (idMap.get(keyInfo.id)); } idMap.set(keyInfo.id, item); }); } function isNameSpecified(componentModel) { var name = componentModel.name; // Is specified when `indexOf` get -1 or > 0. return !!(name && name.indexOf(DUMMY_COMPONENT_NAME_PREFIX)); } /** * @public * @param {Object} cptOption * @return {boolean} */ function isIdInner(cptOption) { return isObject$2(cptOption) && cptOption.id && (cptOption.id + '').indexOf('\0_ec_\0') === 0; } /** * A helper for removing duplicate items between batchA and batchB, * and in themselves, and categorize by series. * * @param {Array.} batchA Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] * @param {Array.} batchB Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] * @return {Array., Array.>} result: [resultBatchA, resultBatchB] */ /** * @param {module:echarts/data/List} data * @param {Object} payload Contains dataIndex (means rawIndex) / dataIndexInside / name * each of which can be Array or primary type. * @return {number|Array.} dataIndex If not found, return undefined/null. */ function queryDataIndex(data, payload) { if (payload.dataIndexInside != null) { return payload.dataIndexInside; } else if (payload.dataIndex != null) { return isArray(payload.dataIndex) ? map(payload.dataIndex, function (value) { return data.indexOfRawIndex(value); }) : data.indexOfRawIndex(payload.dataIndex); } else if (payload.name != null) { return isArray(payload.name) ? map(payload.name, function (value) { return data.indexOfName(value); }) : data.indexOfName(payload.name); } } /** * Enable property storage to any host object. * Notice: Serialization is not supported. * * For example: * var inner = zrUitl.makeInner(); * * function some1(hostObj) { * inner(hostObj).someProperty = 1212; * ... * } * function some2() { * var fields = inner(this); * fields.someProperty1 = 1212; * fields.someProperty2 = 'xx'; * ... * } * * @return {Function} */ function makeInner() { // Consider different scope by es module import. var key = '__\0ec_inner_' + innerUniqueIndex++ + '_' + Math.random().toFixed(5); return function (hostObj) { return hostObj[key] || (hostObj[key] = {}); }; } var innerUniqueIndex = 0; /** * @param {module:echarts/model/Global} ecModel * @param {string|Object} finder * If string, e.g., 'geo', means {geoIndex: 0}. * If Object, could contain some of these properties below: * { * seriesIndex, seriesId, seriesName, * geoIndex, geoId, geoName, * bmapIndex, bmapId, bmapName, * xAxisIndex, xAxisId, xAxisName, * yAxisIndex, yAxisId, yAxisName, * gridIndex, gridId, gridName, * ... (can be extended) * } * Each properties can be number|string|Array.|Array. * For example, a finder could be * { * seriesIndex: 3, * geoId: ['aa', 'cc'], * gridName: ['xx', 'rr'] * } * xxxIndex can be set as 'all' (means all xxx) or 'none' (means not specify) * If nothing or null/undefined specified, return nothing. * @param {Object} [opt] * @param {string} [opt.defaultMainType] * @param {Array.} [opt.includeMainTypes] * @return {Object} result like: * { * seriesModels: [seriesModel1, seriesModel2], * seriesModel: seriesModel1, // The first model * geoModels: [geoModel1, geoModel2], * geoModel: geoModel1, // The first model * ... * } */ function parseFinder(ecModel, finder, opt) { if (isString(finder)) { var obj = {}; obj[finder + 'Index'] = 0; finder = obj; } var defaultMainType = opt && opt.defaultMainType; if (defaultMainType && !has(finder, defaultMainType + 'Index') && !has(finder, defaultMainType + 'Id') && !has(finder, defaultMainType + 'Name') ) { finder[defaultMainType + 'Index'] = 0; } var result = {}; each$2(finder, function (value, key) { var value = finder[key]; // Exclude 'dataIndex' and other illgal keys. if (key === 'dataIndex' || key === 'dataIndexInside') { result[key] = value; return; } var parsedKey = key.match(/^(\w+)(Index|Id|Name)$/) || []; var mainType = parsedKey[1]; var queryType = (parsedKey[2] || '').toLowerCase(); if (!mainType || !queryType || value == null || (queryType === 'index' && value === 'none') || (opt && opt.includeMainTypes && indexOf(opt.includeMainTypes, mainType) < 0) ) { return; } var queryParam = {mainType: mainType}; if (queryType !== 'index' || value !== 'all') { queryParam[queryType] = value; } var models = ecModel.queryComponents(queryParam); result[mainType + 'Models'] = models; result[mainType + 'Model'] = models[0]; }); return result; } function has(obj, prop) { return obj && obj.hasOwnProperty(prop); } function setAttribute(dom, key, value) { dom.setAttribute ? dom.setAttribute(key, value) : (dom[key] = value); } function getAttribute(dom, key) { return dom.getAttribute ? dom.getAttribute(key) : dom[key]; } function getTooltipRenderMode(renderModeOption) { if (renderModeOption === 'auto') { // Using html when `document` exists, use richText otherwise return env$1.domSupported ? 'html' : 'richText'; } else { return renderModeOption || 'html'; } } /** * Group a list by key. * * @param {Array} array * @param {Function} getKey * param {*} Array item * return {string} key * @return {Object} Result * {Array}: keys, * {module:zrender/core/util/HashMap} buckets: {key -> Array} */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var TYPE_DELIMITER = '.'; var IS_CONTAINER = '___EC__COMPONENT__CONTAINER___'; /** * Notice, parseClassType('') should returns {main: '', sub: ''} * @public */ function parseClassType$1(componentType) { var ret = {main: '', sub: ''}; if (componentType) { componentType = componentType.split(TYPE_DELIMITER); ret.main = componentType[0] || ''; ret.sub = componentType[1] || ''; } return ret; } /** * @public */ function checkClassType(componentType) { assert$1( /^[a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)?$/.test(componentType), 'componentType "' + componentType + '" illegal' ); } /** * @public */ function enableClassExtend(RootClass, mandatoryMethods) { RootClass.$constructor = RootClass; RootClass.extend = function (proto) { if (__DEV__) { each$1(mandatoryMethods, function (method) { if (!proto[method]) { console.warn( 'Method `' + method + '` should be implemented' + (proto.type ? ' in ' + proto.type : '') + '.' ); } }); } var superClass = this; var ExtendedClass = function () { if (!proto.$constructor) { superClass.apply(this, arguments); } else { proto.$constructor.apply(this, arguments); } }; extend(ExtendedClass.prototype, proto); ExtendedClass.extend = this.extend; ExtendedClass.superCall = superCall; ExtendedClass.superApply = superApply; inherits(ExtendedClass, this); ExtendedClass.superClass = superClass; return ExtendedClass; }; } var classBase = 0; /** * Can not use instanceof, consider different scope by * cross domain or es module import in ec extensions. * Mount a method "isInstance()" to Clz. */ function enableClassCheck(Clz) { var classAttr = ['__\0is_clz', classBase++, Math.random().toFixed(3)].join('_'); Clz.prototype[classAttr] = true; if (__DEV__) { assert$1(!Clz.isInstance, 'The method "is" can not be defined.'); } Clz.isInstance = function (obj) { return !!(obj && obj[classAttr]); }; } // superCall should have class info, which can not be fetch from 'this'. // Consider this case: // class A has method f, // class B inherits class A, overrides method f, f call superApply('f'), // class C inherits class B, do not overrides method f, // then when method of class C is called, dead loop occured. function superCall(context, methodName) { var args = slice(arguments, 2); return this.superClass.prototype[methodName].apply(context, args); } function superApply(context, methodName, args) { return this.superClass.prototype[methodName].apply(context, args); } /** * @param {Object} entity * @param {Object} options * @param {boolean} [options.registerWhenExtend] * @public */ function enableClassManagement(entity, options) { options = options || {}; /** * Component model classes * key: componentType, * value: * componentClass, when componentType is 'xxx' * or Object., when componentType is 'xxx.yy' * @type {Object} */ var storage = {}; entity.registerClass = function (Clazz, componentType) { if (componentType) { checkClassType(componentType); componentType = parseClassType$1(componentType); if (!componentType.sub) { if (__DEV__) { if (storage[componentType.main]) { console.warn(componentType.main + ' exists.'); } } storage[componentType.main] = Clazz; } else if (componentType.sub !== IS_CONTAINER) { var container = makeContainer(componentType); container[componentType.sub] = Clazz; } } return Clazz; }; entity.getClass = function (componentMainType, subType, throwWhenNotFound) { var Clazz = storage[componentMainType]; if (Clazz && Clazz[IS_CONTAINER]) { Clazz = subType ? Clazz[subType] : null; } if (throwWhenNotFound && !Clazz) { throw new Error( !subType ? componentMainType + '.' + 'type should be specified.' : 'Component ' + componentMainType + '.' + (subType || '') + ' not exists. Load it first.' ); } return Clazz; }; entity.getClassesByMainType = function (componentType) { componentType = parseClassType$1(componentType); var result = []; var obj = storage[componentType.main]; if (obj && obj[IS_CONTAINER]) { each$1(obj, function (o, type) { type !== IS_CONTAINER && result.push(o); }); } else { result.push(obj); } return result; }; entity.hasClass = function (componentType) { // Just consider componentType.main. componentType = parseClassType$1(componentType); return !!storage[componentType.main]; }; /** * @return {Array.} Like ['aa', 'bb'], but can not be ['aa.xx'] */ entity.getAllClassMainTypes = function () { var types = []; each$1(storage, function (obj, type) { types.push(type); }); return types; }; /** * If a main type is container and has sub types * @param {string} mainType * @return {boolean} */ entity.hasSubTypes = function (componentType) { componentType = parseClassType$1(componentType); var obj = storage[componentType.main]; return obj && obj[IS_CONTAINER]; }; entity.parseClassType = parseClassType$1; function makeContainer(componentType) { var container = storage[componentType.main]; if (!container || !container[IS_CONTAINER]) { container = storage[componentType.main] = {}; container[IS_CONTAINER] = true; } return container; } if (options.registerWhenExtend) { var originalExtend = entity.extend; if (originalExtend) { entity.extend = function (proto) { var ExtendedClass = originalExtend.call(this, proto); return entity.registerClass(ExtendedClass, proto.type); }; } } return entity; } /** * @param {string|Array.} properties */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // TODO Parse shadow style // TODO Only shallow path support var makeStyleMapper = function (properties) { // Normalize for (var i = 0; i < properties.length; i++) { if (!properties[i][1]) { properties[i][1] = properties[i][0]; } } return function (model, excludes, includes) { var style = {}; for (var i = 0; i < properties.length; i++) { var propName = properties[i][1]; if ((excludes && indexOf(excludes, propName) >= 0) || (includes && indexOf(includes, propName) < 0) ) { continue; } var val = model.getShallow(propName); if (val != null) { style[properties[i][0]] = val; } } return style; }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var getLineStyle = makeStyleMapper( [ ['lineWidth', 'width'], ['stroke', 'color'], ['opacity'], ['shadowBlur'], ['shadowOffsetX'], ['shadowOffsetY'], ['shadowColor'] ] ); var lineStyleMixin = { getLineStyle: function (excludes) { var style = getLineStyle(this, excludes); // Always set lineDash whether dashed, otherwise we can not // erase the previous style when assigning to el.style. style.lineDash = this.getLineDash(style.lineWidth); return style; }, getLineDash: function (lineWidth) { if (lineWidth == null) { lineWidth = 1; } var lineType = this.get('type'); var dotSize = Math.max(lineWidth, 2); var dashSize = lineWidth * 4; return (lineType === 'solid' || lineType == null) // Use `false` but not `null` for the solid line here, because `null` might be // ignored when assigning to `el.style`. e.g., when setting `lineStyle.type` as // `'dashed'` and `emphasis.lineStyle.type` as `'solid'` in graph series, the // `lineDash` gotten form the latter one is not able to erase that from the former // one if using `null` here according to the emhpsis strategy in `util/graphic.js`. ? false : lineType === 'dashed' ? [dashSize, dashSize] : [dotSize, dotSize]; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var getAreaStyle = makeStyleMapper( [ ['fill', 'color'], ['shadowBlur'], ['shadowOffsetX'], ['shadowOffsetY'], ['opacity'], ['shadowColor'] ] ); var areaStyleMixin = { getAreaStyle: function (excludes, includes) { return getAreaStyle(this, excludes, includes); } }; /** * 曲线辅助模块 * @module zrender/core/curve * @author pissang(https://www.github.com/pissang) */ var mathPow = Math.pow; var mathSqrt$2 = Math.sqrt; var EPSILON$1 = 1e-8; var EPSILON_NUMERIC = 1e-4; var THREE_SQRT = mathSqrt$2(3); var ONE_THIRD = 1 / 3; // 临时变量 var _v0 = create(); var _v1 = create(); var _v2 = create(); function isAroundZero(val) { return val > -EPSILON$1 && val < EPSILON$1; } function isNotAroundZero$1(val) { return val > EPSILON$1 || val < -EPSILON$1; } /** * 计算三次贝塞尔值 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {number} t * @return {number} */ function cubicAt(p0, p1, p2, p3, t) { var onet = 1 - t; return onet * onet * (onet * p0 + 3 * t * p1) + t * t * (t * p3 + 3 * onet * p2); } /** * 计算三次贝塞尔导数值 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {number} t * @return {number} */ function cubicDerivativeAt(p0, p1, p2, p3, t) { var onet = 1 - t; return 3 * ( ((p1 - p0) * onet + 2 * (p2 - p1) * t) * onet + (p3 - p2) * t * t ); } /** * 计算三次贝塞尔方程根,使用盛金公式 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {number} val * @param {Array.} roots * @return {number} 有效根数目 */ function cubicRootAt(p0, p1, p2, p3, val, roots) { // Evaluate roots of cubic functions var a = p3 + 3 * (p1 - p2) - p0; var b = 3 * (p2 - p1 * 2 + p0); var c = 3 * (p1 - p0); var d = p0 - val; var A = b * b - 3 * a * c; var B = b * c - 9 * a * d; var C = c * c - 3 * b * d; var n = 0; if (isAroundZero(A) && isAroundZero(B)) { if (isAroundZero(b)) { roots[0] = 0; } else { var t1 = -c / b; //t1, t2, t3, b is not zero if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } } } else { var disc = B * B - 4 * A * C; if (isAroundZero(disc)) { var K = B / A; var t1 = -b / a + K; // t1, a is not zero var t2 = -K / 2; // t2, t3 if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } if (t2 >= 0 && t2 <= 1) { roots[n++] = t2; } } else if (disc > 0) { var discSqrt = mathSqrt$2(disc); var Y1 = A * b + 1.5 * a * (-B + discSqrt); var Y2 = A * b + 1.5 * a * (-B - discSqrt); if (Y1 < 0) { Y1 = -mathPow(-Y1, ONE_THIRD); } else { Y1 = mathPow(Y1, ONE_THIRD); } if (Y2 < 0) { Y2 = -mathPow(-Y2, ONE_THIRD); } else { Y2 = mathPow(Y2, ONE_THIRD); } var t1 = (-b - (Y1 + Y2)) / (3 * a); if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } } else { var T = (2 * A * b - 3 * a * B) / (2 * mathSqrt$2(A * A * A)); var theta = Math.acos(T) / 3; var ASqrt = mathSqrt$2(A); var tmp = Math.cos(theta); var t1 = (-b - 2 * ASqrt * tmp) / (3 * a); var t2 = (-b + ASqrt * (tmp + THREE_SQRT * Math.sin(theta))) / (3 * a); var t3 = (-b + ASqrt * (tmp - THREE_SQRT * Math.sin(theta))) / (3 * a); if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } if (t2 >= 0 && t2 <= 1) { roots[n++] = t2; } if (t3 >= 0 && t3 <= 1) { roots[n++] = t3; } } } return n; } /** * 计算三次贝塞尔方程极限值的位置 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {Array.} extrema * @return {number} 有效数目 */ function cubicExtrema(p0, p1, p2, p3, extrema) { var b = 6 * p2 - 12 * p1 + 6 * p0; var a = 9 * p1 + 3 * p3 - 3 * p0 - 9 * p2; var c = 3 * p1 - 3 * p0; var n = 0; if (isAroundZero(a)) { if (isNotAroundZero$1(b)) { var t1 = -c / b; if (t1 >= 0 && t1 <= 1) { extrema[n++] = t1; } } } else { var disc = b * b - 4 * a * c; if (isAroundZero(disc)) { extrema[0] = -b / (2 * a); } else if (disc > 0) { var discSqrt = mathSqrt$2(disc); var t1 = (-b + discSqrt) / (2 * a); var t2 = (-b - discSqrt) / (2 * a); if (t1 >= 0 && t1 <= 1) { extrema[n++] = t1; } if (t2 >= 0 && t2 <= 1) { extrema[n++] = t2; } } } return n; } /** * 细分三次贝塞尔曲线 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} p3 * @param {number} t * @param {Array.} out */ function cubicSubdivide(p0, p1, p2, p3, t, out) { var p01 = (p1 - p0) * t + p0; var p12 = (p2 - p1) * t + p1; var p23 = (p3 - p2) * t + p2; var p012 = (p12 - p01) * t + p01; var p123 = (p23 - p12) * t + p12; var p0123 = (p123 - p012) * t + p012; // Seg0 out[0] = p0; out[1] = p01; out[2] = p012; out[3] = p0123; // Seg1 out[4] = p0123; out[5] = p123; out[6] = p23; out[7] = p3; } /** * 投射点到三次贝塞尔曲线上,返回投射距离。 * 投射点有可能会有一个或者多个,这里只返回其中距离最短的一个。 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @param {number} x * @param {number} y * @param {Array.} [out] 投射点 * @return {number} */ function cubicProjectPoint( x0, y0, x1, y1, x2, y2, x3, y3, x, y, out ) { // http://pomax.github.io/bezierinfo/#projections var t; var interval = 0.005; var d = Infinity; var prev; var next; var d1; var d2; _v0[0] = x; _v0[1] = y; // 先粗略估计一下可能的最小距离的 t 值 // PENDING for (var _t = 0; _t < 1; _t += 0.05) { _v1[0] = cubicAt(x0, x1, x2, x3, _t); _v1[1] = cubicAt(y0, y1, y2, y3, _t); d1 = distSquare(_v0, _v1); if (d1 < d) { t = _t; d = d1; } } d = Infinity; // At most 32 iteration for (var i = 0; i < 32; i++) { if (interval < EPSILON_NUMERIC) { break; } prev = t - interval; next = t + interval; // t - interval _v1[0] = cubicAt(x0, x1, x2, x3, prev); _v1[1] = cubicAt(y0, y1, y2, y3, prev); d1 = distSquare(_v1, _v0); if (prev >= 0 && d1 < d) { t = prev; d = d1; } else { // t + interval _v2[0] = cubicAt(x0, x1, x2, x3, next); _v2[1] = cubicAt(y0, y1, y2, y3, next); d2 = distSquare(_v2, _v0); if (next <= 1 && d2 < d) { t = next; d = d2; } else { interval *= 0.5; } } } // t if (out) { out[0] = cubicAt(x0, x1, x2, x3, t); out[1] = cubicAt(y0, y1, y2, y3, t); } // console.log(interval, i); return mathSqrt$2(d); } /** * 计算二次方贝塞尔值 * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} t * @return {number} */ function quadraticAt(p0, p1, p2, t) { var onet = 1 - t; return onet * (onet * p0 + 2 * t * p1) + t * t * p2; } /** * 计算二次方贝塞尔导数值 * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} t * @return {number} */ function quadraticDerivativeAt(p0, p1, p2, t) { return 2 * ((1 - t) * (p1 - p0) + t * (p2 - p1)); } /** * 计算二次方贝塞尔方程根 * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} t * @param {Array.} roots * @return {number} 有效根数目 */ function quadraticRootAt(p0, p1, p2, val, roots) { var a = p0 - 2 * p1 + p2; var b = 2 * (p1 - p0); var c = p0 - val; var n = 0; if (isAroundZero(a)) { if (isNotAroundZero$1(b)) { var t1 = -c / b; if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } } } else { var disc = b * b - 4 * a * c; if (isAroundZero(disc)) { var t1 = -b / (2 * a); if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } } else if (disc > 0) { var discSqrt = mathSqrt$2(disc); var t1 = (-b + discSqrt) / (2 * a); var t2 = (-b - discSqrt) / (2 * a); if (t1 >= 0 && t1 <= 1) { roots[n++] = t1; } if (t2 >= 0 && t2 <= 1) { roots[n++] = t2; } } } return n; } /** * 计算二次贝塞尔方程极限值 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @return {number} */ function quadraticExtremum(p0, p1, p2) { var divider = p0 + p2 - 2 * p1; if (divider === 0) { // p1 is center of p0 and p2 return 0.5; } else { return (p0 - p1) / divider; } } /** * 细分二次贝塞尔曲线 * @memberOf module:zrender/core/curve * @param {number} p0 * @param {number} p1 * @param {number} p2 * @param {number} t * @param {Array.} out */ function quadraticSubdivide(p0, p1, p2, t, out) { var p01 = (p1 - p0) * t + p0; var p12 = (p2 - p1) * t + p1; var p012 = (p12 - p01) * t + p01; // Seg0 out[0] = p0; out[1] = p01; out[2] = p012; // Seg1 out[3] = p012; out[4] = p12; out[5] = p2; } /** * 投射点到二次贝塞尔曲线上,返回投射距离。 * 投射点有可能会有一个或者多个,这里只返回其中距离最短的一个。 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x * @param {number} y * @param {Array.} out 投射点 * @return {number} */ function quadraticProjectPoint( x0, y0, x1, y1, x2, y2, x, y, out ) { // http://pomax.github.io/bezierinfo/#projections var t; var interval = 0.005; var d = Infinity; _v0[0] = x; _v0[1] = y; // 先粗略估计一下可能的最小距离的 t 值 // PENDING for (var _t = 0; _t < 1; _t += 0.05) { _v1[0] = quadraticAt(x0, x1, x2, _t); _v1[1] = quadraticAt(y0, y1, y2, _t); var d1 = distSquare(_v0, _v1); if (d1 < d) { t = _t; d = d1; } } d = Infinity; // At most 32 iteration for (var i = 0; i < 32; i++) { if (interval < EPSILON_NUMERIC) { break; } var prev = t - interval; var next = t + interval; // t - interval _v1[0] = quadraticAt(x0, x1, x2, prev); _v1[1] = quadraticAt(y0, y1, y2, prev); var d1 = distSquare(_v1, _v0); if (prev >= 0 && d1 < d) { t = prev; d = d1; } else { // t + interval _v2[0] = quadraticAt(x0, x1, x2, next); _v2[1] = quadraticAt(y0, y1, y2, next); var d2 = distSquare(_v2, _v0); if (next <= 1 && d2 < d) { t = next; d = d2; } else { interval *= 0.5; } } } // t if (out) { out[0] = quadraticAt(x0, x1, x2, t); out[1] = quadraticAt(y0, y1, y2, t); } // console.log(interval, i); return mathSqrt$2(d); } /** * @author Yi Shen(https://github.com/pissang) */ var mathMin$3 = Math.min; var mathMax$3 = Math.max; var mathSin$2 = Math.sin; var mathCos$2 = Math.cos; var PI2 = Math.PI * 2; var start = create(); var end = create(); var extremity = create(); /** * 从顶点数组中计算出最小包围盒,写入`min`和`max`中 * @module zrender/core/bbox * @param {Array} points 顶点数组 * @param {number} min * @param {number} max */ function fromPoints(points, min$$1, max$$1) { if (points.length === 0) { return; } var p = points[0]; var left = p[0]; var right = p[0]; var top = p[1]; var bottom = p[1]; var i; for (i = 1; i < points.length; i++) { p = points[i]; left = mathMin$3(left, p[0]); right = mathMax$3(right, p[0]); top = mathMin$3(top, p[1]); bottom = mathMax$3(bottom, p[1]); } min$$1[0] = left; min$$1[1] = top; max$$1[0] = right; max$$1[1] = bottom; } /** * @memberOf module:zrender/core/bbox * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {Array.} min * @param {Array.} max */ function fromLine(x0, y0, x1, y1, min$$1, max$$1) { min$$1[0] = mathMin$3(x0, x1); min$$1[1] = mathMin$3(y0, y1); max$$1[0] = mathMax$3(x0, x1); max$$1[1] = mathMax$3(y0, y1); } var xDim = []; var yDim = []; /** * 从三阶贝塞尔曲线(p0, p1, p2, p3)中计算出最小包围盒,写入`min`和`max`中 * @memberOf module:zrender/core/bbox * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @param {Array.} min * @param {Array.} max */ function fromCubic( x0, y0, x1, y1, x2, y2, x3, y3, min$$1, max$$1 ) { var cubicExtrema$$1 = cubicExtrema; var cubicAt$$1 = cubicAt; var i; var n = cubicExtrema$$1(x0, x1, x2, x3, xDim); min$$1[0] = Infinity; min$$1[1] = Infinity; max$$1[0] = -Infinity; max$$1[1] = -Infinity; for (i = 0; i < n; i++) { var x = cubicAt$$1(x0, x1, x2, x3, xDim[i]); min$$1[0] = mathMin$3(x, min$$1[0]); max$$1[0] = mathMax$3(x, max$$1[0]); } n = cubicExtrema$$1(y0, y1, y2, y3, yDim); for (i = 0; i < n; i++) { var y = cubicAt$$1(y0, y1, y2, y3, yDim[i]); min$$1[1] = mathMin$3(y, min$$1[1]); max$$1[1] = mathMax$3(y, max$$1[1]); } min$$1[0] = mathMin$3(x0, min$$1[0]); max$$1[0] = mathMax$3(x0, max$$1[0]); min$$1[0] = mathMin$3(x3, min$$1[0]); max$$1[0] = mathMax$3(x3, max$$1[0]); min$$1[1] = mathMin$3(y0, min$$1[1]); max$$1[1] = mathMax$3(y0, max$$1[1]); min$$1[1] = mathMin$3(y3, min$$1[1]); max$$1[1] = mathMax$3(y3, max$$1[1]); } /** * 从二阶贝塞尔曲线(p0, p1, p2)中计算出最小包围盒,写入`min`和`max`中 * @memberOf module:zrender/core/bbox * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {Array.} min * @param {Array.} max */ function fromQuadratic(x0, y0, x1, y1, x2, y2, min$$1, max$$1) { var quadraticExtremum$$1 = quadraticExtremum; var quadraticAt$$1 = quadraticAt; // Find extremities, where derivative in x dim or y dim is zero var tx = mathMax$3( mathMin$3(quadraticExtremum$$1(x0, x1, x2), 1), 0 ); var ty = mathMax$3( mathMin$3(quadraticExtremum$$1(y0, y1, y2), 1), 0 ); var x = quadraticAt$$1(x0, x1, x2, tx); var y = quadraticAt$$1(y0, y1, y2, ty); min$$1[0] = mathMin$3(x0, x2, x); min$$1[1] = mathMin$3(y0, y2, y); max$$1[0] = mathMax$3(x0, x2, x); max$$1[1] = mathMax$3(y0, y2, y); } /** * 从圆弧中计算出最小包围盒,写入`min`和`max`中 * @method * @memberOf module:zrender/core/bbox * @param {number} x * @param {number} y * @param {number} rx * @param {number} ry * @param {number} startAngle * @param {number} endAngle * @param {number} anticlockwise * @param {Array.} min * @param {Array.} max */ function fromArc( x, y, rx, ry, startAngle, endAngle, anticlockwise, min$$1, max$$1 ) { var vec2Min = min; var vec2Max = max; var diff = Math.abs(startAngle - endAngle); if (diff % PI2 < 1e-4 && diff > 1e-4) { // Is a circle min$$1[0] = x - rx; min$$1[1] = y - ry; max$$1[0] = x + rx; max$$1[1] = y + ry; return; } start[0] = mathCos$2(startAngle) * rx + x; start[1] = mathSin$2(startAngle) * ry + y; end[0] = mathCos$2(endAngle) * rx + x; end[1] = mathSin$2(endAngle) * ry + y; vec2Min(min$$1, start, end); vec2Max(max$$1, start, end); // Thresh to [0, Math.PI * 2] startAngle = startAngle % (PI2); if (startAngle < 0) { startAngle = startAngle + PI2; } endAngle = endAngle % (PI2); if (endAngle < 0) { endAngle = endAngle + PI2; } if (startAngle > endAngle && !anticlockwise) { endAngle += PI2; } else if (startAngle < endAngle && anticlockwise) { startAngle += PI2; } if (anticlockwise) { var tmp = endAngle; endAngle = startAngle; startAngle = tmp; } // var number = 0; // var step = (anticlockwise ? -Math.PI : Math.PI) / 2; for (var angle = 0; angle < endAngle; angle += Math.PI / 2) { if (angle > startAngle) { extremity[0] = mathCos$2(angle) * rx + x; extremity[1] = mathSin$2(angle) * ry + y; vec2Min(min$$1, extremity, min$$1); vec2Max(max$$1, extremity, max$$1); } } } /** * Path 代理,可以在`buildPath`中用于替代`ctx`, 会保存每个path操作的命令到pathCommands属性中 * 可以用于 isInsidePath 判断以及获取boundingRect * * @module zrender/core/PathProxy * @author Yi Shen (http://www.github.com/pissang) */ // TODO getTotalLength, getPointAtLength /* global Float32Array */ var CMD = { M: 1, L: 2, C: 3, Q: 4, A: 5, Z: 6, // Rect R: 7 }; // var CMD_MEM_SIZE = { // M: 3, // L: 3, // C: 7, // Q: 5, // A: 9, // R: 5, // Z: 1 // }; var min$1 = []; var max$1 = []; var min2 = []; var max2 = []; var mathMin$2 = Math.min; var mathMax$2 = Math.max; var mathCos$1 = Math.cos; var mathSin$1 = Math.sin; var mathSqrt$1 = Math.sqrt; var mathAbs = Math.abs; var hasTypedArray = typeof Float32Array !== 'undefined'; /** * @alias module:zrender/core/PathProxy * @constructor */ var PathProxy = function (notSaveData) { this._saveData = !(notSaveData || false); if (this._saveData) { /** * Path data. Stored as flat array * @type {Array.} */ this.data = []; } this._ctx = null; }; /** * 快速计算Path包围盒(并不是最小包围盒) * @return {Object} */ PathProxy.prototype = { constructor: PathProxy, _xi: 0, _yi: 0, _x0: 0, _y0: 0, // Unit x, Unit y. Provide for avoiding drawing that too short line segment _ux: 0, _uy: 0, _len: 0, _lineDash: null, _dashOffset: 0, _dashIdx: 0, _dashSum: 0, /** * @readOnly */ setScale: function (sx, sy, segmentIgnoreThreshold) { // Compat. Previously there is no segmentIgnoreThreshold. segmentIgnoreThreshold = segmentIgnoreThreshold || 0; this._ux = mathAbs(segmentIgnoreThreshold / devicePixelRatio / sx) || 0; this._uy = mathAbs(segmentIgnoreThreshold / devicePixelRatio / sy) || 0; }, getContext: function () { return this._ctx; }, /** * @param {CanvasRenderingContext2D} ctx * @return {module:zrender/core/PathProxy} */ beginPath: function (ctx) { this._ctx = ctx; ctx && ctx.beginPath(); ctx && (this.dpr = ctx.dpr); // Reset if (this._saveData) { this._len = 0; } if (this._lineDash) { this._lineDash = null; this._dashOffset = 0; } return this; }, /** * @param {number} x * @param {number} y * @return {module:zrender/core/PathProxy} */ moveTo: function (x, y) { this.addData(CMD.M, x, y); this._ctx && this._ctx.moveTo(x, y); // x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用 // xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。 // 有可能在 beginPath 之后直接调用 lineTo,这时候 x0, y0 需要 // 在 lineTo 方法中记录,这里先不考虑这种情况,dashed line 也只在 IE10- 中不支持 this._x0 = x; this._y0 = y; this._xi = x; this._yi = y; return this; }, /** * @param {number} x * @param {number} y * @return {module:zrender/core/PathProxy} */ lineTo: function (x, y) { var exceedUnit = mathAbs(x - this._xi) > this._ux || mathAbs(y - this._yi) > this._uy // Force draw the first segment || this._len < 5; this.addData(CMD.L, x, y); if (this._ctx && exceedUnit) { this._needsDash() ? this._dashedLineTo(x, y) : this._ctx.lineTo(x, y); } if (exceedUnit) { this._xi = x; this._yi = y; } return this; }, /** * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @return {module:zrender/core/PathProxy} */ bezierCurveTo: function (x1, y1, x2, y2, x3, y3) { this.addData(CMD.C, x1, y1, x2, y2, x3, y3); if (this._ctx) { this._needsDash() ? this._dashedBezierTo(x1, y1, x2, y2, x3, y3) : this._ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); } this._xi = x3; this._yi = y3; return this; }, /** * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @return {module:zrender/core/PathProxy} */ quadraticCurveTo: function (x1, y1, x2, y2) { this.addData(CMD.Q, x1, y1, x2, y2); if (this._ctx) { this._needsDash() ? this._dashedQuadraticTo(x1, y1, x2, y2) : this._ctx.quadraticCurveTo(x1, y1, x2, y2); } this._xi = x2; this._yi = y2; return this; }, /** * @param {number} cx * @param {number} cy * @param {number} r * @param {number} startAngle * @param {number} endAngle * @param {boolean} anticlockwise * @return {module:zrender/core/PathProxy} */ arc: function (cx, cy, r, startAngle, endAngle, anticlockwise) { this.addData( CMD.A, cx, cy, r, r, startAngle, endAngle - startAngle, 0, anticlockwise ? 0 : 1 ); this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise); this._xi = mathCos$1(endAngle) * r + cx; this._yi = mathSin$1(endAngle) * r + cy; return this; }, // TODO arcTo: function (x1, y1, x2, y2, radius) { if (this._ctx) { this._ctx.arcTo(x1, y1, x2, y2, radius); } return this; }, // TODO rect: function (x, y, w, h) { this._ctx && this._ctx.rect(x, y, w, h); this.addData(CMD.R, x, y, w, h); return this; }, /** * @return {module:zrender/core/PathProxy} */ closePath: function () { this.addData(CMD.Z); var ctx = this._ctx; var x0 = this._x0; var y0 = this._y0; if (ctx) { this._needsDash() && this._dashedLineTo(x0, y0); ctx.closePath(); } this._xi = x0; this._yi = y0; return this; }, /** * Context 从外部传入,因为有可能是 rebuildPath 完之后再 fill。 * stroke 同样 * @param {CanvasRenderingContext2D} ctx * @return {module:zrender/core/PathProxy} */ fill: function (ctx) { ctx && ctx.fill(); this.toStatic(); }, /** * @param {CanvasRenderingContext2D} ctx * @return {module:zrender/core/PathProxy} */ stroke: function (ctx) { ctx && ctx.stroke(); this.toStatic(); }, /** * 必须在其它绘制命令前调用 * Must be invoked before all other path drawing methods * @return {module:zrender/core/PathProxy} */ setLineDash: function (lineDash) { if (lineDash instanceof Array) { this._lineDash = lineDash; this._dashIdx = 0; var lineDashSum = 0; for (var i = 0; i < lineDash.length; i++) { lineDashSum += lineDash[i]; } this._dashSum = lineDashSum; } return this; }, /** * 必须在其它绘制命令前调用 * Must be invoked before all other path drawing methods * @return {module:zrender/core/PathProxy} */ setLineDashOffset: function (offset) { this._dashOffset = offset; return this; }, /** * * @return {boolean} */ len: function () { return this._len; }, /** * 直接设置 Path 数据 */ setData: function (data) { var len$$1 = data.length; if (!(this.data && this.data.length === len$$1) && hasTypedArray) { this.data = new Float32Array(len$$1); } for (var i = 0; i < len$$1; i++) { this.data[i] = data[i]; } this._len = len$$1; }, /** * 添加子路径 * @param {module:zrender/core/PathProxy|Array.} path */ appendPath: function (path) { if (!(path instanceof Array)) { path = [path]; } var len$$1 = path.length; var appendSize = 0; var offset = this._len; for (var i = 0; i < len$$1; i++) { appendSize += path[i].len(); } if (hasTypedArray && (this.data instanceof Float32Array)) { this.data = new Float32Array(offset + appendSize); } for (var i = 0; i < len$$1; i++) { var appendPathData = path[i].data; for (var k = 0; k < appendPathData.length; k++) { this.data[offset++] = appendPathData[k]; } } this._len = offset; }, /** * 填充 Path 数据。 * 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。 */ addData: function (cmd) { if (!this._saveData) { return; } var data = this.data; if (this._len + arguments.length > data.length) { // 因为之前的数组已经转换成静态的 Float32Array // 所以不够用时需要扩展一个新的动态数组 this._expandData(); data = this.data; } for (var i = 0; i < arguments.length; i++) { data[this._len++] = arguments[i]; } this._prevCmd = cmd; }, _expandData: function () { // Only if data is Float32Array if (!(this.data instanceof Array)) { var newData = []; for (var i = 0; i < this._len; i++) { newData[i] = this.data[i]; } this.data = newData; } }, /** * If needs js implemented dashed line * @return {boolean} * @private */ _needsDash: function () { return this._lineDash; }, _dashedLineTo: function (x1, y1) { var dashSum = this._dashSum; var offset = this._dashOffset; var lineDash = this._lineDash; var ctx = this._ctx; var x0 = this._xi; var y0 = this._yi; var dx = x1 - x0; var dy = y1 - y0; var dist$$1 = mathSqrt$1(dx * dx + dy * dy); var x = x0; var y = y0; var dash; var nDash = lineDash.length; var idx; dx /= dist$$1; dy /= dist$$1; if (offset < 0) { // Convert to positive offset offset = dashSum + offset; } offset %= dashSum; x -= offset * dx; y -= offset * dy; while ((dx > 0 && x <= x1) || (dx < 0 && x >= x1) || (dx === 0 && ((dy > 0 && y <= y1) || (dy < 0 && y >= y1)))) { idx = this._dashIdx; dash = lineDash[idx]; x += dx * dash; y += dy * dash; this._dashIdx = (idx + 1) % nDash; // Skip positive offset if ((dx > 0 && x < x0) || (dx < 0 && x > x0) || (dy > 0 && y < y0) || (dy < 0 && y > y0)) { continue; } ctx[idx % 2 ? 'moveTo' : 'lineTo']( dx >= 0 ? mathMin$2(x, x1) : mathMax$2(x, x1), dy >= 0 ? mathMin$2(y, y1) : mathMax$2(y, y1) ); } // Offset for next lineTo dx = x - x1; dy = y - y1; this._dashOffset = -mathSqrt$1(dx * dx + dy * dy); }, // Not accurate dashed line to _dashedBezierTo: function (x1, y1, x2, y2, x3, y3) { var dashSum = this._dashSum; var offset = this._dashOffset; var lineDash = this._lineDash; var ctx = this._ctx; var x0 = this._xi; var y0 = this._yi; var t; var dx; var dy; var cubicAt$$1 = cubicAt; var bezierLen = 0; var idx = this._dashIdx; var nDash = lineDash.length; var x; var y; var tmpLen = 0; if (offset < 0) { // Convert to positive offset offset = dashSum + offset; } offset %= dashSum; // Bezier approx length for (t = 0; t < 1; t += 0.1) { dx = cubicAt$$1(x0, x1, x2, x3, t + 0.1) - cubicAt$$1(x0, x1, x2, x3, t); dy = cubicAt$$1(y0, y1, y2, y3, t + 0.1) - cubicAt$$1(y0, y1, y2, y3, t); bezierLen += mathSqrt$1(dx * dx + dy * dy); } // Find idx after add offset for (; idx < nDash; idx++) { tmpLen += lineDash[idx]; if (tmpLen > offset) { break; } } t = (tmpLen - offset) / bezierLen; while (t <= 1) { x = cubicAt$$1(x0, x1, x2, x3, t); y = cubicAt$$1(y0, y1, y2, y3, t); // Use line to approximate dashed bezier // Bad result if dash is long idx % 2 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); t += lineDash[idx] / bezierLen; idx = (idx + 1) % nDash; } // Finish the last segment and calculate the new offset (idx % 2 !== 0) && ctx.lineTo(x3, y3); dx = x3 - x; dy = y3 - y; this._dashOffset = -mathSqrt$1(dx * dx + dy * dy); }, _dashedQuadraticTo: function (x1, y1, x2, y2) { // Convert quadratic to cubic using degree elevation var x3 = x2; var y3 = y2; x2 = (x2 + 2 * x1) / 3; y2 = (y2 + 2 * y1) / 3; x1 = (this._xi + 2 * x1) / 3; y1 = (this._yi + 2 * y1) / 3; this._dashedBezierTo(x1, y1, x2, y2, x3, y3); }, /** * 转成静态的 Float32Array 减少堆内存占用 * Convert dynamic array to static Float32Array */ toStatic: function () { var data = this.data; if (data instanceof Array) { data.length = this._len; if (hasTypedArray) { this.data = new Float32Array(data); } } }, /** * @return {module:zrender/core/BoundingRect} */ getBoundingRect: function () { min$1[0] = min$1[1] = min2[0] = min2[1] = Number.MAX_VALUE; max$1[0] = max$1[1] = max2[0] = max2[1] = -Number.MAX_VALUE; var data = this.data; var xi = 0; var yi = 0; var x0 = 0; var y0 = 0; for (var i = 0; i < data.length;) { var cmd = data[i++]; if (i === 1) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = data[i]; yi = data[i + 1]; x0 = xi; y0 = yi; } switch (cmd) { case CMD.M: // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 // 在 closePath 的时候使用 x0 = data[i++]; y0 = data[i++]; xi = x0; yi = y0; min2[0] = x0; min2[1] = y0; max2[0] = x0; max2[1] = y0; break; case CMD.L: fromLine(xi, yi, data[i], data[i + 1], min2, max2); xi = data[i++]; yi = data[i++]; break; case CMD.C: fromCubic( xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], min2, max2 ); xi = data[i++]; yi = data[i++]; break; case CMD.Q: fromQuadratic( xi, yi, data[i++], data[i++], data[i], data[i + 1], min2, max2 ); xi = data[i++]; yi = data[i++]; break; case CMD.A: // TODO Arc 判断的开销比较大 var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var startAngle = data[i++]; var endAngle = data[i++] + startAngle; // TODO Arc 旋转 i += 1; var anticlockwise = 1 - data[i++]; if (i === 1) { // 直接使用 arc 命令 // 第一个命令起点还未定义 x0 = mathCos$1(startAngle) * rx + cx; y0 = mathSin$1(startAngle) * ry + cy; } fromArc( cx, cy, rx, ry, startAngle, endAngle, anticlockwise, min2, max2 ); xi = mathCos$1(endAngle) * rx + cx; yi = mathSin$1(endAngle) * ry + cy; break; case CMD.R: x0 = xi = data[i++]; y0 = yi = data[i++]; var width = data[i++]; var height = data[i++]; // Use fromLine fromLine(x0, y0, x0 + width, y0 + height, min2, max2); break; case CMD.Z: xi = x0; yi = y0; break; } // Union min(min$1, min$1, min2); max(max$1, max$1, max2); } // No data if (i === 0) { min$1[0] = min$1[1] = max$1[0] = max$1[1] = 0; } return new BoundingRect( min$1[0], min$1[1], max$1[0] - min$1[0], max$1[1] - min$1[1] ); }, /** * Rebuild path from current data * Rebuild path will not consider javascript implemented line dash. * @param {CanvasRenderingContext2D} ctx */ rebuildPath: function (ctx) { var d = this.data; var x0; var y0; var xi; var yi; var x; var y; var ux = this._ux; var uy = this._uy; var len$$1 = this._len; for (var i = 0; i < len$$1;) { var cmd = d[i++]; if (i === 1) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = d[i]; yi = d[i + 1]; x0 = xi; y0 = yi; } switch (cmd) { case CMD.M: x0 = xi = d[i++]; y0 = yi = d[i++]; ctx.moveTo(xi, yi); break; case CMD.L: x = d[i++]; y = d[i++]; // Not draw too small seg between if (mathAbs(x - xi) > ux || mathAbs(y - yi) > uy || i === len$$1 - 1) { ctx.lineTo(x, y); xi = x; yi = y; } break; case CMD.C: ctx.bezierCurveTo( d[i++], d[i++], d[i++], d[i++], d[i++], d[i++] ); xi = d[i - 2]; yi = d[i - 1]; break; case CMD.Q: ctx.quadraticCurveTo(d[i++], d[i++], d[i++], d[i++]); xi = d[i - 2]; yi = d[i - 1]; break; case CMD.A: var cx = d[i++]; var cy = d[i++]; var rx = d[i++]; var ry = d[i++]; var theta = d[i++]; var dTheta = d[i++]; var psi = d[i++]; var fs = d[i++]; var r = (rx > ry) ? rx : ry; var scaleX = (rx > ry) ? 1 : rx / ry; var scaleY = (rx > ry) ? ry / rx : 1; var isEllipse = Math.abs(rx - ry) > 1e-3; var endAngle = theta + dTheta; if (isEllipse) { ctx.translate(cx, cy); ctx.rotate(psi); ctx.scale(scaleX, scaleY); ctx.arc(0, 0, r, theta, endAngle, 1 - fs); ctx.scale(1 / scaleX, 1 / scaleY); ctx.rotate(-psi); ctx.translate(-cx, -cy); } else { ctx.arc(cx, cy, r, theta, endAngle, 1 - fs); } if (i === 1) { // 直接使用 arc 命令 // 第一个命令起点还未定义 x0 = mathCos$1(theta) * rx + cx; y0 = mathSin$1(theta) * ry + cy; } xi = mathCos$1(endAngle) * rx + cx; yi = mathSin$1(endAngle) * ry + cy; break; case CMD.R: x0 = xi = d[i]; y0 = yi = d[i + 1]; ctx.rect(d[i++], d[i++], d[i++], d[i++]); break; case CMD.Z: ctx.closePath(); xi = x0; yi = y0; } } } }; PathProxy.CMD = CMD; /** * 线段包含判断 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} lineWidth * @param {number} x * @param {number} y * @return {boolean} */ function containStroke$1(x0, y0, x1, y1, lineWidth, x, y) { if (lineWidth === 0) { return false; } var _l = lineWidth; var _a = 0; var _b = x0; // Quick reject if ( (y > y0 + _l && y > y1 + _l) || (y < y0 - _l && y < y1 - _l) || (x > x0 + _l && x > x1 + _l) || (x < x0 - _l && x < x1 - _l) ) { return false; } if (x0 !== x1) { _a = (y0 - y1) / (x0 - x1); _b = (x0 * y1 - x1 * y0) / (x0 - x1); } else { return Math.abs(x - x0) <= _l / 2; } var tmp = _a * x - y + _b; var _s = tmp * tmp / (_a * _a + 1); return _s <= _l / 2 * _l / 2; } /** * 三次贝塞尔曲线描边包含判断 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @param {number} lineWidth * @param {number} x * @param {number} y * @return {boolean} */ function containStroke$2(x0, y0, x1, y1, x2, y2, x3, y3, lineWidth, x, y) { if (lineWidth === 0) { return false; } var _l = lineWidth; // Quick reject if ( (y > y0 + _l && y > y1 + _l && y > y2 + _l && y > y3 + _l) || (y < y0 - _l && y < y1 - _l && y < y2 - _l && y < y3 - _l) || (x > x0 + _l && x > x1 + _l && x > x2 + _l && x > x3 + _l) || (x < x0 - _l && x < x1 - _l && x < x2 - _l && x < x3 - _l) ) { return false; } var d = cubicProjectPoint( x0, y0, x1, y1, x2, y2, x3, y3, x, y, null ); return d <= _l / 2; } /** * 二次贝塞尔曲线描边包含判断 * @param {number} x0 * @param {number} y0 * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} lineWidth * @param {number} x * @param {number} y * @return {boolean} */ function containStroke$3(x0, y0, x1, y1, x2, y2, lineWidth, x, y) { if (lineWidth === 0) { return false; } var _l = lineWidth; // Quick reject if ( (y > y0 + _l && y > y1 + _l && y > y2 + _l) || (y < y0 - _l && y < y1 - _l && y < y2 - _l) || (x > x0 + _l && x > x1 + _l && x > x2 + _l) || (x < x0 - _l && x < x1 - _l && x < x2 - _l) ) { return false; } var d = quadraticProjectPoint( x0, y0, x1, y1, x2, y2, x, y, null ); return d <= _l / 2; } var PI2$3 = Math.PI * 2; function normalizeRadian(angle) { angle %= PI2$3; if (angle < 0) { angle += PI2$3; } return angle; } var PI2$2 = Math.PI * 2; /** * 圆弧描边包含判断 * @param {number} cx * @param {number} cy * @param {number} r * @param {number} startAngle * @param {number} endAngle * @param {boolean} anticlockwise * @param {number} lineWidth * @param {number} x * @param {number} y * @return {Boolean} */ function containStroke$4( cx, cy, r, startAngle, endAngle, anticlockwise, lineWidth, x, y ) { if (lineWidth === 0) { return false; } var _l = lineWidth; x -= cx; y -= cy; var d = Math.sqrt(x * x + y * y); if ((d - _l > r) || (d + _l < r)) { return false; } if (Math.abs(startAngle - endAngle) % PI2$2 < 1e-4) { // Is a circle return true; } if (anticlockwise) { var tmp = startAngle; startAngle = normalizeRadian(endAngle); endAngle = normalizeRadian(tmp); } else { startAngle = normalizeRadian(startAngle); endAngle = normalizeRadian(endAngle); } if (startAngle > endAngle) { endAngle += PI2$2; } var angle = Math.atan2(y, x); if (angle < 0) { angle += PI2$2; } return (angle >= startAngle && angle <= endAngle) || (angle + PI2$2 >= startAngle && angle + PI2$2 <= endAngle); } function windingLine(x0, y0, x1, y1, x, y) { if ((y > y0 && y > y1) || (y < y0 && y < y1)) { return 0; } // Ignore horizontal line if (y1 === y0) { return 0; } var dir = y1 < y0 ? 1 : -1; var t = (y - y0) / (y1 - y0); // Avoid winding error when intersection point is the connect point of two line of polygon if (t === 1 || t === 0) { dir = y1 < y0 ? 0.5 : -0.5; } var x_ = t * (x1 - x0) + x0; // If (x, y) on the line, considered as "contain". return x_ === x ? Infinity : x_ > x ? dir : 0; } var CMD$1 = PathProxy.CMD; var PI2$1 = Math.PI * 2; var EPSILON$2 = 1e-4; function isAroundEqual(a, b) { return Math.abs(a - b) < EPSILON$2; } // 临时数组 var roots = [-1, -1, -1]; var extrema = [-1, -1]; function swapExtrema() { var tmp = extrema[0]; extrema[0] = extrema[1]; extrema[1] = tmp; } function windingCubic(x0, y0, x1, y1, x2, y2, x3, y3, x, y) { // Quick reject if ( (y > y0 && y > y1 && y > y2 && y > y3) || (y < y0 && y < y1 && y < y2 && y < y3) ) { return 0; } var nRoots = cubicRootAt(y0, y1, y2, y3, y, roots); if (nRoots === 0) { return 0; } else { var w = 0; var nExtrema = -1; var y0_; var y1_; for (var i = 0; i < nRoots; i++) { var t = roots[i]; // Avoid winding error when intersection point is the connect point of two line of polygon var unit = (t === 0 || t === 1) ? 0.5 : 1; var x_ = cubicAt(x0, x1, x2, x3, t); if (x_ < x) { // Quick reject continue; } if (nExtrema < 0) { nExtrema = cubicExtrema(y0, y1, y2, y3, extrema); if (extrema[1] < extrema[0] && nExtrema > 1) { swapExtrema(); } y0_ = cubicAt(y0, y1, y2, y3, extrema[0]); if (nExtrema > 1) { y1_ = cubicAt(y0, y1, y2, y3, extrema[1]); } } if (nExtrema === 2) { // 分成三段单调函数 if (t < extrema[0]) { w += y0_ < y0 ? unit : -unit; } else if (t < extrema[1]) { w += y1_ < y0_ ? unit : -unit; } else { w += y3 < y1_ ? unit : -unit; } } else { // 分成两段单调函数 if (t < extrema[0]) { w += y0_ < y0 ? unit : -unit; } else { w += y3 < y0_ ? unit : -unit; } } } return w; } } function windingQuadratic(x0, y0, x1, y1, x2, y2, x, y) { // Quick reject if ( (y > y0 && y > y1 && y > y2) || (y < y0 && y < y1 && y < y2) ) { return 0; } var nRoots = quadraticRootAt(y0, y1, y2, y, roots); if (nRoots === 0) { return 0; } else { var t = quadraticExtremum(y0, y1, y2); if (t >= 0 && t <= 1) { var w = 0; var y_ = quadraticAt(y0, y1, y2, t); for (var i = 0; i < nRoots; i++) { // Remove one endpoint. var unit = (roots[i] === 0 || roots[i] === 1) ? 0.5 : 1; var x_ = quadraticAt(x0, x1, x2, roots[i]); if (x_ < x) { // Quick reject continue; } if (roots[i] < t) { w += y_ < y0 ? unit : -unit; } else { w += y2 < y_ ? unit : -unit; } } return w; } else { // Remove one endpoint. var unit = (roots[0] === 0 || roots[0] === 1) ? 0.5 : 1; var x_ = quadraticAt(x0, x1, x2, roots[0]); if (x_ < x) { // Quick reject return 0; } return y2 < y0 ? unit : -unit; } } } // TODO // Arc 旋转 function windingArc( cx, cy, r, startAngle, endAngle, anticlockwise, x, y ) { y -= cy; if (y > r || y < -r) { return 0; } var tmp = Math.sqrt(r * r - y * y); roots[0] = -tmp; roots[1] = tmp; var diff = Math.abs(startAngle - endAngle); if (diff < 1e-4) { return 0; } if (diff % PI2$1 < 1e-4) { // Is a circle startAngle = 0; endAngle = PI2$1; var dir = anticlockwise ? 1 : -1; if (x >= roots[0] + cx && x <= roots[1] + cx) { return dir; } else { return 0; } } if (anticlockwise) { var tmp = startAngle; startAngle = normalizeRadian(endAngle); endAngle = normalizeRadian(tmp); } else { startAngle = normalizeRadian(startAngle); endAngle = normalizeRadian(endAngle); } if (startAngle > endAngle) { endAngle += PI2$1; } var w = 0; for (var i = 0; i < 2; i++) { var x_ = roots[i]; if (x_ + cx > x) { var angle = Math.atan2(y, x_); var dir = anticlockwise ? 1 : -1; if (angle < 0) { angle = PI2$1 + angle; } if ( (angle >= startAngle && angle <= endAngle) || (angle + PI2$1 >= startAngle && angle + PI2$1 <= endAngle) ) { if (angle > Math.PI / 2 && angle < Math.PI * 1.5) { dir = -dir; } w += dir; } } } return w; } function containPath(data, lineWidth, isStroke, x, y) { var w = 0; var xi = 0; var yi = 0; var x0 = 0; var y0 = 0; for (var i = 0; i < data.length;) { var cmd = data[i++]; // Begin a new subpath if (cmd === CMD$1.M && i > 1) { // Close previous subpath if (!isStroke) { w += windingLine(xi, yi, x0, y0, x, y); } // 如果被任何一个 subpath 包含 // if (w !== 0) { // return true; // } } if (i === 1) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = data[i]; yi = data[i + 1]; x0 = xi; y0 = yi; } switch (cmd) { case CMD$1.M: // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 // 在 closePath 的时候使用 x0 = data[i++]; y0 = data[i++]; xi = x0; yi = y0; break; case CMD$1.L: if (isStroke) { if (containStroke$1(xi, yi, data[i], data[i + 1], lineWidth, x, y)) { return true; } } else { // NOTE 在第一个命令为 L, C, Q 的时候会计算出 NaN w += windingLine(xi, yi, data[i], data[i + 1], x, y) || 0; } xi = data[i++]; yi = data[i++]; break; case CMD$1.C: if (isStroke) { if (containStroke$2(xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], lineWidth, x, y )) { return true; } } else { w += windingCubic( xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], x, y ) || 0; } xi = data[i++]; yi = data[i++]; break; case CMD$1.Q: if (isStroke) { if (containStroke$3(xi, yi, data[i++], data[i++], data[i], data[i + 1], lineWidth, x, y )) { return true; } } else { w += windingQuadratic( xi, yi, data[i++], data[i++], data[i], data[i + 1], x, y ) || 0; } xi = data[i++]; yi = data[i++]; break; case CMD$1.A: // TODO Arc 判断的开销比较大 var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var theta = data[i++]; var dTheta = data[i++]; // TODO Arc 旋转 i += 1; var anticlockwise = 1 - data[i++]; var x1 = Math.cos(theta) * rx + cx; var y1 = Math.sin(theta) * ry + cy; // 不是直接使用 arc 命令 if (i > 1) { w += windingLine(xi, yi, x1, y1, x, y); } else { // 第一个命令起点还未定义 x0 = x1; y0 = y1; } // zr 使用scale来模拟椭圆, 这里也对x做一定的缩放 var _x = (x - cx) * ry / rx + cx; if (isStroke) { if (containStroke$4( cx, cy, ry, theta, theta + dTheta, anticlockwise, lineWidth, _x, y )) { return true; } } else { w += windingArc( cx, cy, ry, theta, theta + dTheta, anticlockwise, _x, y ); } xi = Math.cos(theta + dTheta) * rx + cx; yi = Math.sin(theta + dTheta) * ry + cy; break; case CMD$1.R: x0 = xi = data[i++]; y0 = yi = data[i++]; var width = data[i++]; var height = data[i++]; var x1 = x0 + width; var y1 = y0 + height; if (isStroke) { if (containStroke$1(x0, y0, x1, y0, lineWidth, x, y) || containStroke$1(x1, y0, x1, y1, lineWidth, x, y) || containStroke$1(x1, y1, x0, y1, lineWidth, x, y) || containStroke$1(x0, y1, x0, y0, lineWidth, x, y) ) { return true; } } else { // FIXME Clockwise ? w += windingLine(x1, y0, x1, y1, x, y); w += windingLine(x0, y1, x0, y0, x, y); } break; case CMD$1.Z: if (isStroke) { if (containStroke$1( xi, yi, x0, y0, lineWidth, x, y )) { return true; } } else { // Close a subpath w += windingLine(xi, yi, x0, y0, x, y); // 如果被任何一个 subpath 包含 // FIXME subpaths may overlap // if (w !== 0) { // return true; // } } xi = x0; yi = y0; break; } } if (!isStroke && !isAroundEqual(yi, y0)) { w += windingLine(xi, yi, x0, y0, x, y) || 0; } return w !== 0; } function contain(pathData, x, y) { return containPath(pathData, 0, false, x, y); } function containStroke(pathData, lineWidth, x, y) { return containPath(pathData, lineWidth, true, x, y); } var getCanvasPattern = Pattern.prototype.getCanvasPattern; var abs = Math.abs; var pathProxyForDraw = new PathProxy(true); /** * @alias module:zrender/graphic/Path * @extends module:zrender/graphic/Displayable * @constructor * @param {Object} opts */ function Path(opts) { Displayable.call(this, opts); /** * @type {module:zrender/core/PathProxy} * @readOnly */ this.path = null; } Path.prototype = { constructor: Path, type: 'path', __dirtyPath: true, strokeContainThreshold: 5, // This item default to be false. But in map series in echarts, // in order to improve performance, it should be set to true, // so the shorty segment won't draw. segmentIgnoreThreshold: 0, /** * See `module:zrender/src/graphic/helper/subPixelOptimize`. * @type {boolean} */ subPixelOptimize: false, brush: function (ctx, prevEl) { var style = this.style; var path = this.path || pathProxyForDraw; var hasStroke = style.hasStroke(); var hasFill = style.hasFill(); var fill = style.fill; var stroke = style.stroke; var hasFillGradient = hasFill && !!(fill.colorStops); var hasStrokeGradient = hasStroke && !!(stroke.colorStops); var hasFillPattern = hasFill && !!(fill.image); var hasStrokePattern = hasStroke && !!(stroke.image); style.bind(ctx, this, prevEl); this.setTransform(ctx); if (this.__dirty) { var rect; // Update gradient because bounding rect may changed if (hasFillGradient) { rect = rect || this.getBoundingRect(); this._fillGradient = style.getGradient(ctx, fill, rect); } if (hasStrokeGradient) { rect = rect || this.getBoundingRect(); this._strokeGradient = style.getGradient(ctx, stroke, rect); } } // Use the gradient or pattern if (hasFillGradient) { // PENDING If may have affect the state ctx.fillStyle = this._fillGradient; } else if (hasFillPattern) { ctx.fillStyle = getCanvasPattern.call(fill, ctx); } if (hasStrokeGradient) { ctx.strokeStyle = this._strokeGradient; } else if (hasStrokePattern) { ctx.strokeStyle = getCanvasPattern.call(stroke, ctx); } var lineDash = style.lineDash; var lineDashOffset = style.lineDashOffset; var ctxLineDash = !!ctx.setLineDash; // Update path sx, sy var scale = this.getGlobalScale(); path.setScale(scale[0], scale[1], this.segmentIgnoreThreshold); // Proxy context // Rebuild path in following 2 cases // 1. Path is dirty // 2. Path needs javascript implemented lineDash stroking. // In this case, lineDash information will not be saved in PathProxy if (this.__dirtyPath || (lineDash && !ctxLineDash && hasStroke) ) { path.beginPath(ctx); // Setting line dash before build path if (lineDash && !ctxLineDash) { path.setLineDash(lineDash); path.setLineDashOffset(lineDashOffset); } this.buildPath(path, this.shape, false); // Clear path dirty flag if (this.path) { this.__dirtyPath = false; } } else { // Replay path building ctx.beginPath(); this.path.rebuildPath(ctx); } if (hasFill) { if (style.fillOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.fillOpacity * style.opacity; path.fill(ctx); ctx.globalAlpha = originalGlobalAlpha; } else { path.fill(ctx); } } if (lineDash && ctxLineDash) { ctx.setLineDash(lineDash); ctx.lineDashOffset = lineDashOffset; } if (hasStroke) { if (style.strokeOpacity != null) { var originalGlobalAlpha = ctx.globalAlpha; ctx.globalAlpha = style.strokeOpacity * style.opacity; path.stroke(ctx); ctx.globalAlpha = originalGlobalAlpha; } else { path.stroke(ctx); } } if (lineDash && ctxLineDash) { // PENDING // Remove lineDash ctx.setLineDash([]); } // Draw rect text if (style.text != null) { // Only restore transform when needs draw text. this.restoreTransform(ctx); this.drawRectText(ctx, this.getBoundingRect()); } }, // When bundling path, some shape may decide if use moveTo to begin a new subpath or closePath // Like in circle buildPath: function (ctx, shapeCfg, inBundle) {}, createPathProxy: function () { this.path = new PathProxy(); }, getBoundingRect: function () { var rect = this._rect; var style = this.style; var needsUpdateRect = !rect; if (needsUpdateRect) { var path = this.path; if (!path) { // Create path on demand. path = this.path = new PathProxy(); } if (this.__dirtyPath) { path.beginPath(); this.buildPath(path, this.shape, false); } rect = path.getBoundingRect(); } this._rect = rect; if (style.hasStroke()) { // Needs update rect with stroke lineWidth when // 1. Element changes scale or lineWidth // 2. Shape is changed var rectWithStroke = this._rectWithStroke || (this._rectWithStroke = rect.clone()); if (this.__dirty || needsUpdateRect) { rectWithStroke.copy(rect); // FIXME Must after updateTransform var w = style.lineWidth; // PENDING, Min line width is needed when line is horizontal or vertical var lineScale = style.strokeNoScale ? this.getLineScale() : 1; // Only add extra hover lineWidth when there are no fill if (!style.hasFill()) { w = Math.max(w, this.strokeContainThreshold || 4); } // Consider line width // Line scale can't be 0; if (lineScale > 1e-10) { rectWithStroke.width += w / lineScale; rectWithStroke.height += w / lineScale; rectWithStroke.x -= w / lineScale / 2; rectWithStroke.y -= w / lineScale / 2; } } // Return rect with stroke return rectWithStroke; } return rect; }, contain: function (x, y) { var localPos = this.transformCoordToLocal(x, y); var rect = this.getBoundingRect(); var style = this.style; x = localPos[0]; y = localPos[1]; if (rect.contain(x, y)) { var pathData = this.path.data; if (style.hasStroke()) { var lineWidth = style.lineWidth; var lineScale = style.strokeNoScale ? this.getLineScale() : 1; // Line scale can't be 0; if (lineScale > 1e-10) { // Only add extra hover lineWidth when there are no fill if (!style.hasFill()) { lineWidth = Math.max(lineWidth, this.strokeContainThreshold); } if (containStroke( pathData, lineWidth / lineScale, x, y )) { return true; } } } if (style.hasFill()) { return contain(pathData, x, y); } } return false; }, /** * @param {boolean} dirtyPath */ dirty: function (dirtyPath) { if (dirtyPath == null) { dirtyPath = true; } // Only mark dirty, not mark clean if (dirtyPath) { this.__dirtyPath = dirtyPath; this._rect = null; } this.__dirty = this.__dirtyText = true; this.__zr && this.__zr.refresh(); // Used as a clipping path if (this.__clipTarget) { this.__clipTarget.dirty(); } }, /** * Alias for animate('shape') * @param {boolean} loop */ animateShape: function (loop) { return this.animate('shape', loop); }, // Overwrite attrKV attrKV: function (key, value) { // FIXME if (key === 'shape') { this.setShape(value); this.__dirtyPath = true; this._rect = null; } else { Displayable.prototype.attrKV.call(this, key, value); } }, /** * @param {Object|string} key * @param {*} value */ setShape: function (key, value) { var shape = this.shape; // Path from string may not have shape if (shape) { if (isObject$1(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { shape[name] = key[name]; } } } else { shape[key] = value; } this.dirty(true); } return this; }, getLineScale: function () { var m = this.transform; // Get the line scale. // Determinant of `m` means how much the area is enlarged by the // transformation. So its square root can be used as a scale factor // for width. return m && abs(m[0] - 1) > 1e-10 && abs(m[3] - 1) > 1e-10 ? Math.sqrt(abs(m[0] * m[3] - m[2] * m[1])) : 1; } }; /** * 扩展一个 Path element, 比如星形,圆等。 * Extend a path element * @param {Object} props * @param {string} props.type Path type * @param {Function} props.init Initialize * @param {Function} props.buildPath Overwrite buildPath method * @param {Object} [props.style] Extended default style config * @param {Object} [props.shape] Extended default shape config */ Path.extend = function (defaults$$1) { var Sub = function (opts) { Path.call(this, opts); if (defaults$$1.style) { // Extend default style this.style.extendFrom(defaults$$1.style, false); } // Extend default shape var defaultShape = defaults$$1.shape; if (defaultShape) { this.shape = this.shape || {}; var thisShape = this.shape; for (var name in defaultShape) { if ( !thisShape.hasOwnProperty(name) && defaultShape.hasOwnProperty(name) ) { thisShape[name] = defaultShape[name]; } } } defaults$$1.init && defaults$$1.init.call(this, opts); }; inherits(Sub, Path); // FIXME 不能 extend position, rotation 等引用对象 for (var name in defaults$$1) { // Extending prototype values and methods if (name !== 'style' && name !== 'shape') { Sub.prototype[name] = defaults$$1[name]; } } return Sub; }; inherits(Path, Displayable); var CMD$2 = PathProxy.CMD; var points = [[], [], []]; var mathSqrt$3 = Math.sqrt; var mathAtan2 = Math.atan2; var transformPath = function (path, m) { var data = path.data; var cmd; var nPoint; var i; var j; var k; var p; var M = CMD$2.M; var C = CMD$2.C; var L = CMD$2.L; var R = CMD$2.R; var A = CMD$2.A; var Q = CMD$2.Q; for (i = 0, j = 0; i < data.length;) { cmd = data[i++]; j = i; nPoint = 0; switch (cmd) { case M: nPoint = 1; break; case L: nPoint = 1; break; case C: nPoint = 3; break; case Q: nPoint = 2; break; case A: var x = m[4]; var y = m[5]; var sx = mathSqrt$3(m[0] * m[0] + m[1] * m[1]); var sy = mathSqrt$3(m[2] * m[2] + m[3] * m[3]); var angle = mathAtan2(-m[1] / sy, m[0] / sx); // cx data[i] *= sx; data[i++] += x; // cy data[i] *= sy; data[i++] += y; // Scale rx and ry // FIXME Assume psi is 0 here data[i++] *= sx; data[i++] *= sy; // Start angle data[i++] += angle; // end angle data[i++] += angle; // FIXME psi i += 2; j = i; break; case R: // x0, y0 p[0] = data[i++]; p[1] = data[i++]; applyTransform(p, p, m); data[j++] = p[0]; data[j++] = p[1]; // x1, y1 p[0] += data[i++]; p[1] += data[i++]; applyTransform(p, p, m); data[j++] = p[0]; data[j++] = p[1]; } for (k = 0; k < nPoint; k++) { var p = points[k]; p[0] = data[i++]; p[1] = data[i++]; applyTransform(p, p, m); // Write back data[j++] = p[0]; data[j++] = p[1]; } } }; // command chars // var cc = [ // 'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', // 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A' // ]; var mathSqrt = Math.sqrt; var mathSin = Math.sin; var mathCos = Math.cos; var PI = Math.PI; var vMag = function (v) { return Math.sqrt(v[0] * v[0] + v[1] * v[1]); }; var vRatio = function (u, v) { return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); }; var vAngle = function (u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); }; function processArc(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg, cmd, path) { var psi = psiDeg * (PI / 180.0); var xp = mathCos(psi) * (x1 - x2) / 2.0 + mathSin(psi) * (y1 - y2) / 2.0; var yp = -1 * mathSin(psi) * (x1 - x2) / 2.0 + mathCos(psi) * (y1 - y2) / 2.0; var lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry); if (lambda > 1) { rx *= mathSqrt(lambda); ry *= mathSqrt(lambda); } var f = (fa === fs ? -1 : 1) * mathSqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp)) ) || 0; var cxp = f * rx * yp / ry; var cyp = f * -ry * xp / rx; var cx = (x1 + x2) / 2.0 + mathCos(psi) * cxp - mathSin(psi) * cyp; var cy = (y1 + y2) / 2.0 + mathSin(psi) * cxp + mathCos(psi) * cyp; var theta = vAngle([ 1, 0 ], [ (xp - cxp) / rx, (yp - cyp) / ry ]); var u = [ (xp - cxp) / rx, (yp - cyp) / ry ]; var v = [ (-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry ]; var dTheta = vAngle(u, v); if (vRatio(u, v) <= -1) { dTheta = PI; } if (vRatio(u, v) >= 1) { dTheta = 0; } if (fs === 0 && dTheta > 0) { dTheta = dTheta - 2 * PI; } if (fs === 1 && dTheta < 0) { dTheta = dTheta + 2 * PI; } path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs); } var commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig; // Consider case: // (1) delimiter can be comma or space, where continuous commas // or spaces should be seen as one comma. // (2) value can be like: // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983', // 'l-.5E1,54', '121-23-44-11' (no delimiter) var numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g; // var valueSplitReg = /[\s,]+/; function createPathProxyFromString(data) { if (!data) { return new PathProxy(); } // var data = data.replace(/-/g, ' -') // .replace(/ /g, ' ') // .replace(/ /g, ',') // .replace(/,,/g, ','); // var n; // create pipes so that we can split the data // for (n = 0; n < cc.length; n++) { // cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); // } // data = data.replace(/-/g, ',-'); // create array // var arr = cs.split('|'); // init context point var cpx = 0; var cpy = 0; var subpathX = cpx; var subpathY = cpy; var prevCmd; var path = new PathProxy(); var CMD = PathProxy.CMD; // commandReg.lastIndex = 0; // var cmdResult; // while ((cmdResult = commandReg.exec(data)) != null) { // var cmdStr = cmdResult[1]; // var cmdContent = cmdResult[2]; var cmdList = data.match(commandReg); for (var l = 0; l < cmdList.length; l++) { var cmdText = cmdList[l]; var cmdStr = cmdText.charAt(0); var cmd; // String#split is faster a little bit than String#replace or RegExp#exec. // var p = cmdContent.split(valueSplitReg); // var pLen = 0; // for (var i = 0; i < p.length; i++) { // // '' and other invalid str => NaN // var val = parseFloat(p[i]); // !isNaN(val) && (p[pLen++] = val); // } var p = cmdText.match(numberReg) || []; var pLen = p.length; for (var i = 0; i < pLen; i++) { p[i] = parseFloat(p[i]); } var off = 0; while (off < pLen) { var ctlPtx; var ctlPty; var rx; var ry; var psi; var fa; var fs; var x1 = cpx; var y1 = cpy; // convert l, H, h, V, and v to L switch (cmdStr) { case 'l': cpx += p[off++]; cpy += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'L': cpx = p[off++]; cpy = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'm': cpx += p[off++]; cpy += p[off++]; cmd = CMD.M; path.addData(cmd, cpx, cpy); subpathX = cpx; subpathY = cpy; cmdStr = 'l'; break; case 'M': cpx = p[off++]; cpy = p[off++]; cmd = CMD.M; path.addData(cmd, cpx, cpy); subpathX = cpx; subpathY = cpy; cmdStr = 'L'; break; case 'h': cpx += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'H': cpx = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'v': cpy += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'V': cpy = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'C': cmd = CMD.C; path.addData( cmd, p[off++], p[off++], p[off++], p[off++], p[off++], p[off++] ); cpx = p[off - 2]; cpy = p[off - 1]; break; case 'c': cmd = CMD.C; path.addData( cmd, p[off++] + cpx, p[off++] + cpy, p[off++] + cpx, p[off++] + cpy, p[off++] + cpx, p[off++] + cpy ); cpx += p[off - 2]; cpy += p[off - 1]; break; case 'S': ctlPtx = cpx; ctlPty = cpy; var len = path.len(); var pathData = path.data; if (prevCmd === CMD.C) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cmd = CMD.C; x1 = p[off++]; y1 = p[off++]; cpx = p[off++]; cpy = p[off++]; path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy); break; case 's': ctlPtx = cpx; ctlPty = cpy; var len = path.len(); var pathData = path.data; if (prevCmd === CMD.C) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cmd = CMD.C; x1 = cpx + p[off++]; y1 = cpy + p[off++]; cpx += p[off++]; cpy += p[off++]; path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy); break; case 'Q': x1 = p[off++]; y1 = p[off++]; cpx = p[off++]; cpy = p[off++]; cmd = CMD.Q; path.addData(cmd, x1, y1, cpx, cpy); break; case 'q': x1 = p[off++] + cpx; y1 = p[off++] + cpy; cpx += p[off++]; cpy += p[off++]; cmd = CMD.Q; path.addData(cmd, x1, y1, cpx, cpy); break; case 'T': ctlPtx = cpx; ctlPty = cpy; var len = path.len(); var pathData = path.data; if (prevCmd === CMD.Q) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cpx = p[off++]; cpy = p[off++]; cmd = CMD.Q; path.addData(cmd, ctlPtx, ctlPty, cpx, cpy); break; case 't': ctlPtx = cpx; ctlPty = cpy; var len = path.len(); var pathData = path.data; if (prevCmd === CMD.Q) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cpx += p[off++]; cpy += p[off++]; cmd = CMD.Q; path.addData(cmd, ctlPtx, ctlPty, cpx, cpy); break; case 'A': rx = p[off++]; ry = p[off++]; psi = p[off++]; fa = p[off++]; fs = p[off++]; x1 = cpx, y1 = cpy; cpx = p[off++]; cpy = p[off++]; cmd = CMD.A; processArc( x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path ); break; case 'a': rx = p[off++]; ry = p[off++]; psi = p[off++]; fa = p[off++]; fs = p[off++]; x1 = cpx, y1 = cpy; cpx += p[off++]; cpy += p[off++]; cmd = CMD.A; processArc( x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path ); break; } } if (cmdStr === 'z' || cmdStr === 'Z') { cmd = CMD.Z; path.addData(cmd); // z may be in the middle of the path. cpx = subpathX; cpy = subpathY; } prevCmd = cmd; } path.toStatic(); return path; } // TODO Optimize double memory cost problem function createPathOptions(str, opts) { var pathProxy = createPathProxyFromString(str); opts = opts || {}; opts.buildPath = function (path) { if (path.setData) { path.setData(pathProxy.data); // Svg and vml renderer don't have context var ctx = path.getContext(); if (ctx) { path.rebuildPath(ctx); } } else { var ctx = path; pathProxy.rebuildPath(ctx); } }; opts.applyTransform = function (m) { transformPath(pathProxy, m); this.dirty(true); }; return opts; } /** * Create a Path object from path string data * http://www.w3.org/TR/SVG/paths.html#PathData * @param {Object} opts Other options */ function createFromString(str, opts) { return new Path(createPathOptions(str, opts)); } /** * Create a Path class from path string data * @param {string} str * @param {Object} opts Other options */ function extendFromString(str, opts) { return Path.extend(createPathOptions(str, opts)); } /** * Merge multiple paths */ // TODO Apply transform // TODO stroke dash // TODO Optimize double memory cost problem function mergePath$1(pathEls, opts) { var pathList = []; var len = pathEls.length; for (var i = 0; i < len; i++) { var pathEl = pathEls[i]; if (!pathEl.path) { pathEl.createPathProxy(); } if (pathEl.__dirtyPath) { pathEl.buildPath(pathEl.path, pathEl.shape, true); } pathList.push(pathEl.path); } var pathBundle = new Path(opts); // Need path proxy. pathBundle.createPathProxy(); pathBundle.buildPath = function (path) { path.appendPath(pathList); // Svg and vml renderer don't have context var ctx = path.getContext(); if (ctx) { path.rebuildPath(ctx); } }; return pathBundle; } /** * @alias zrender/graphic/Text * @extends module:zrender/graphic/Displayable * @constructor * @param {Object} opts */ var Text = function (opts) { // jshint ignore:line Displayable.call(this, opts); }; Text.prototype = { constructor: Text, type: 'text', brush: function (ctx, prevEl) { var style = this.style; // Optimize, avoid normalize every time. this.__dirty && normalizeTextStyle(style, true); // Use props with prefix 'text'. style.fill = style.stroke = style.shadowBlur = style.shadowColor = style.shadowOffsetX = style.shadowOffsetY = null; var text = style.text; // Convert to string text != null && (text += ''); // Do not apply style.bind in Text node. Because the real bind job // is in textHelper.renderText, and performance of text render should // be considered. // style.bind(ctx, this, prevEl); if (!needDrawText(text, style)) { // The current el.style is not applied // and should not be used as cache. ctx.__attrCachedBy = ContextCachedBy.NONE; return; } this.setTransform(ctx); renderText(this, ctx, text, style, null, prevEl); this.restoreTransform(ctx); }, getBoundingRect: function () { var style = this.style; // Optimize, avoid normalize every time. this.__dirty && normalizeTextStyle(style, true); if (!this._rect) { var text = style.text; text != null ? (text += '') : (text = ''); var rect = getBoundingRect( style.text + '', style.font, style.textAlign, style.textVerticalAlign, style.textPadding, style.textLineHeight, style.rich ); rect.x += style.x || 0; rect.y += style.y || 0; if (getStroke(style.textStroke, style.textStrokeWidth)) { var w = style.textStrokeWidth; rect.x -= w / 2; rect.y -= w / 2; rect.width += w; rect.height += w; } this._rect = rect; } return this._rect; } }; inherits(Text, Displayable); /** * 圆形 * @module zrender/shape/Circle */ var Circle = Path.extend({ type: 'circle', shape: { cx: 0, cy: 0, r: 0 }, buildPath: function (ctx, shape, inBundle) { // Better stroking in ShapeBundle // Always do it may have performence issue ( fill may be 2x more cost) if (inBundle) { ctx.moveTo(shape.cx + shape.r, shape.cy); } // else { // if (ctx.allocate && !ctx.data.length) { // ctx.allocate(ctx.CMD_MEM_SIZE.A); // } // } // Better stroking in ShapeBundle // ctx.moveTo(shape.cx + shape.r, shape.cy); ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2, true); } }); // Fix weird bug in some version of IE11 (like 11.0.9600.178**), // where exception "unexpected call to method or property access" // might be thrown when calling ctx.fill or ctx.stroke after a path // whose area size is zero is drawn and ctx.clip() is called and // shadowBlur is set. See #4572, #3112, #5777. // (e.g., // ctx.moveTo(10, 10); // ctx.lineTo(20, 10); // ctx.closePath(); // ctx.clip(); // ctx.shadowBlur = 10; // ... // ctx.fill(); // ) var shadowTemp = [ ['shadowBlur', 0], ['shadowColor', '#000'], ['shadowOffsetX', 0], ['shadowOffsetY', 0] ]; var fixClipWithShadow = function (orignalBrush) { // version string can be: '11.0' return (env$1.browser.ie && env$1.browser.version >= 11) ? function () { var clipPaths = this.__clipPaths; var style = this.style; var modified; if (clipPaths) { for (var i = 0; i < clipPaths.length; i++) { var clipPath = clipPaths[i]; var shape = clipPath && clipPath.shape; var type = clipPath && clipPath.type; if (shape && ( (type === 'sector' && shape.startAngle === shape.endAngle) || (type === 'rect' && (!shape.width || !shape.height)) )) { for (var j = 0; j < shadowTemp.length; j++) { // It is save to put shadowTemp static, because shadowTemp // will be all modified each item brush called. shadowTemp[j][2] = style[shadowTemp[j][0]]; style[shadowTemp[j][0]] = shadowTemp[j][1]; } modified = true; break; } } } orignalBrush.apply(this, arguments); if (modified) { for (var j = 0; j < shadowTemp.length; j++) { style[shadowTemp[j][0]] = shadowTemp[j][2]; } } } : orignalBrush; }; /** * 扇形 * @module zrender/graphic/shape/Sector */ var Sector = Path.extend({ type: 'sector', shape: { cx: 0, cy: 0, r0: 0, r: 0, startAngle: 0, endAngle: Math.PI * 2, clockwise: true }, brush: fixClipWithShadow(Path.prototype.brush), buildPath: function (ctx, shape) { var x = shape.cx; var y = shape.cy; var r0 = Math.max(shape.r0 || 0, 0); var r = Math.max(shape.r, 0); var startAngle = shape.startAngle; var endAngle = shape.endAngle; var clockwise = shape.clockwise; var unitX = Math.cos(startAngle); var unitY = Math.sin(startAngle); ctx.moveTo(unitX * r0 + x, unitY * r0 + y); ctx.lineTo(unitX * r + x, unitY * r + y); ctx.arc(x, y, r, startAngle, endAngle, !clockwise); ctx.lineTo( Math.cos(endAngle) * r0 + x, Math.sin(endAngle) * r0 + y ); if (r0 !== 0) { ctx.arc(x, y, r0, endAngle, startAngle, clockwise); } ctx.closePath(); } }); /** * 圆环 * @module zrender/graphic/shape/Ring */ var Ring = Path.extend({ type: 'ring', shape: { cx: 0, cy: 0, r: 0, r0: 0 }, buildPath: function (ctx, shape) { var x = shape.cx; var y = shape.cy; var PI2 = Math.PI * 2; ctx.moveTo(x + shape.r, y); ctx.arc(x, y, shape.r, 0, PI2, false); ctx.moveTo(x + shape.r0, y); ctx.arc(x, y, shape.r0, 0, PI2, true); } }); /** * Catmull-Rom spline 插值折线 * @module zrender/shape/util/smoothSpline * @author pissang (https://www.github.com/pissang) * Kener (@Kener-林峰, kener.linfeng@gmail.com) * errorrik (errorrik@gmail.com) */ /** * @inner */ function interpolate(p0, p1, p2, p3, t, t2, t3) { var v0 = (p2 - p0) * 0.5; var v1 = (p3 - p1) * 0.5; return (2 * (p1 - p2) + v0 + v1) * t3 + (-3 * (p1 - p2) - 2 * v0 - v1) * t2 + v0 * t + p1; } /** * @alias module:zrender/shape/util/smoothSpline * @param {Array} points 线段顶点数组 * @param {boolean} isLoop * @return {Array} */ var smoothSpline = function (points, isLoop) { var len$$1 = points.length; var ret = []; var distance$$1 = 0; for (var i = 1; i < len$$1; i++) { distance$$1 += distance(points[i - 1], points[i]); } var segs = distance$$1 / 2; segs = segs < len$$1 ? len$$1 : segs; for (var i = 0; i < segs; i++) { var pos = i / (segs - 1) * (isLoop ? len$$1 : len$$1 - 1); var idx = Math.floor(pos); var w = pos - idx; var p0; var p1 = points[idx % len$$1]; var p2; var p3; if (!isLoop) { p0 = points[idx === 0 ? idx : idx - 1]; p2 = points[idx > len$$1 - 2 ? len$$1 - 1 : idx + 1]; p3 = points[idx > len$$1 - 3 ? len$$1 - 1 : idx + 2]; } else { p0 = points[(idx - 1 + len$$1) % len$$1]; p2 = points[(idx + 1) % len$$1]; p3 = points[(idx + 2) % len$$1]; } var w2 = w * w; var w3 = w * w2; ret.push([ interpolate(p0[0], p1[0], p2[0], p3[0], w, w2, w3), interpolate(p0[1], p1[1], p2[1], p3[1], w, w2, w3) ]); } return ret; }; /** * 贝塞尔平滑曲线 * @module zrender/shape/util/smoothBezier * @author pissang (https://www.github.com/pissang) * Kener (@Kener-林峰, kener.linfeng@gmail.com) * errorrik (errorrik@gmail.com) */ /** * 贝塞尔平滑曲线 * @alias module:zrender/shape/util/smoothBezier * @param {Array} points 线段顶点数组 * @param {number} smooth 平滑等级, 0-1 * @param {boolean} isLoop * @param {Array} constraint 将计算出来的控制点约束在一个包围盒内 * 比如 [[0, 0], [100, 100]], 这个包围盒会与 * 整个折线的包围盒做一个并集用来约束控制点。 * @param {Array} 计算出来的控制点数组 */ var smoothBezier = function (points, smooth, isLoop, constraint) { var cps = []; var v = []; var v1 = []; var v2 = []; var prevPoint; var nextPoint; var min$$1; var max$$1; if (constraint) { min$$1 = [Infinity, Infinity]; max$$1 = [-Infinity, -Infinity]; for (var i = 0, len$$1 = points.length; i < len$$1; i++) { min(min$$1, min$$1, points[i]); max(max$$1, max$$1, points[i]); } // 与指定的包围盒做并集 min(min$$1, min$$1, constraint[0]); max(max$$1, max$$1, constraint[1]); } for (var i = 0, len$$1 = points.length; i < len$$1; i++) { var point = points[i]; if (isLoop) { prevPoint = points[i ? i - 1 : len$$1 - 1]; nextPoint = points[(i + 1) % len$$1]; } else { if (i === 0 || i === len$$1 - 1) { cps.push(clone$1(points[i])); continue; } else { prevPoint = points[i - 1]; nextPoint = points[i + 1]; } } sub(v, nextPoint, prevPoint); // use degree to scale the handle length scale(v, v, smooth); var d0 = distance(point, prevPoint); var d1 = distance(point, nextPoint); var sum = d0 + d1; if (sum !== 0) { d0 /= sum; d1 /= sum; } scale(v1, v, -d0); scale(v2, v, d1); var cp0 = add([], point, v1); var cp1 = add([], point, v2); if (constraint) { max(cp0, cp0, min$$1); min(cp0, cp0, max$$1); max(cp1, cp1, min$$1); min(cp1, cp1, max$$1); } cps.push(cp0); cps.push(cp1); } if (isLoop) { cps.push(cps.shift()); } return cps; }; function buildPath$1(ctx, shape, closePath) { var points = shape.points; var smooth = shape.smooth; if (points && points.length >= 2) { if (smooth && smooth !== 'spline') { var controlPoints = smoothBezier( points, smooth, closePath, shape.smoothConstraint ); ctx.moveTo(points[0][0], points[0][1]); var len = points.length; for (var i = 0; i < (closePath ? len : len - 1); i++) { var cp1 = controlPoints[i * 2]; var cp2 = controlPoints[i * 2 + 1]; var p = points[(i + 1) % len]; ctx.bezierCurveTo( cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1] ); } } else { if (smooth === 'spline') { points = smoothSpline(points, closePath); } ctx.moveTo(points[0][0], points[0][1]); for (var i = 1, l = points.length; i < l; i++) { ctx.lineTo(points[i][0], points[i][1]); } } closePath && ctx.closePath(); } } /** * 多边形 * @module zrender/shape/Polygon */ var Polygon = Path.extend({ type: 'polygon', shape: { points: null, smooth: false, smoothConstraint: null }, buildPath: function (ctx, shape) { buildPath$1(ctx, shape, true); } }); /** * @module zrender/graphic/shape/Polyline */ var Polyline = Path.extend({ type: 'polyline', shape: { points: null, smooth: false, smoothConstraint: null }, style: { stroke: '#000', fill: null }, buildPath: function (ctx, shape) { buildPath$1(ctx, shape, false); } }); /** * Sub-pixel optimize for canvas rendering, prevent from blur * when rendering a thin vertical/horizontal line. */ var round = Math.round; /** * Sub pixel optimize line for canvas * * @param {Object} outputShape The modification will be performed on `outputShape`. * `outputShape` and `inputShape` can be the same object. * `outputShape` object can be used repeatly, because all of * the `x1`, `x2`, `y1`, `y2` will be assigned in this method. * @param {Object} [inputShape] * @param {number} [inputShape.x1] * @param {number} [inputShape.y1] * @param {number} [inputShape.x2] * @param {number} [inputShape.y2] * @param {Object} [style] * @param {number} [style.lineWidth] If `null`/`undefined`/`0`, do not optimize. */ function subPixelOptimizeLine$1(outputShape, inputShape, style) { if (!inputShape) { return; } var x1 = inputShape.x1; var x2 = inputShape.x2; var y1 = inputShape.y1; var y2 = inputShape.y2; outputShape.x1 = x1; outputShape.x2 = x2; outputShape.y1 = y1; outputShape.y2 = y2; var lineWidth = style && style.lineWidth; if (!lineWidth) { return; } if (round(x1 * 2) === round(x2 * 2)) { outputShape.x1 = outputShape.x2 = subPixelOptimize$1(x1, lineWidth, true); } if (round(y1 * 2) === round(y2 * 2)) { outputShape.y1 = outputShape.y2 = subPixelOptimize$1(y1, lineWidth, true); } } /** * Sub pixel optimize rect for canvas * * @param {Object} outputShape The modification will be performed on `outputShape`. * `outputShape` and `inputShape` can be the same object. * `outputShape` object can be used repeatly, because all of * the `x`, `y`, `width`, `height` will be assigned in this method. * @param {Object} [inputShape] * @param {number} [inputShape.x] * @param {number} [inputShape.y] * @param {number} [inputShape.width] * @param {number} [inputShape.height] * @param {Object} [style] * @param {number} [style.lineWidth] If `null`/`undefined`/`0`, do not optimize. */ function subPixelOptimizeRect$1(outputShape, inputShape, style) { if (!inputShape) { return; } var originX = inputShape.x; var originY = inputShape.y; var originWidth = inputShape.width; var originHeight = inputShape.height; outputShape.x = originX; outputShape.y = originY; outputShape.width = originWidth; outputShape.height = originHeight; var lineWidth = style && style.lineWidth; if (!lineWidth) { return; } outputShape.x = subPixelOptimize$1(originX, lineWidth, true); outputShape.y = subPixelOptimize$1(originY, lineWidth, true); outputShape.width = Math.max( subPixelOptimize$1(originX + originWidth, lineWidth, false) - outputShape.x, originWidth === 0 ? 0 : 1 ); outputShape.height = Math.max( subPixelOptimize$1(originY + originHeight, lineWidth, false) - outputShape.y, originHeight === 0 ? 0 : 1 ); } /** * Sub pixel optimize for canvas * * @param {number} position Coordinate, such as x, y * @param {number} lineWidth If `null`/`undefined`/`0`, do not optimize. * @param {boolean=} positiveOrNegative Default false (negative). * @return {number} Optimized position. */ function subPixelOptimize$1(position, lineWidth, positiveOrNegative) { if (!lineWidth) { return position; } // Assure that (position + lineWidth / 2) is near integer edge, // otherwise line will be fuzzy in canvas. var doubledPosition = round(position * 2); return (doubledPosition + round(lineWidth)) % 2 === 0 ? doubledPosition / 2 : (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2; } /** * 矩形 * @module zrender/graphic/shape/Rect */ // Avoid create repeatly. var subPixelOptimizeOutputShape = {}; var Rect = Path.extend({ type: 'rect', shape: { // 左上、右上、右下、左下角的半径依次为r1、r2、r3、r4 // r缩写为1 相当于 [1, 1, 1, 1] // r缩写为[1] 相当于 [1, 1, 1, 1] // r缩写为[1, 2] 相当于 [1, 2, 1, 2] // r缩写为[1, 2, 3] 相当于 [1, 2, 3, 2] r: 0, x: 0, y: 0, width: 0, height: 0 }, buildPath: function (ctx, shape) { var x; var y; var width; var height; if (this.subPixelOptimize) { subPixelOptimizeRect$1(subPixelOptimizeOutputShape, shape, this.style); x = subPixelOptimizeOutputShape.x; y = subPixelOptimizeOutputShape.y; width = subPixelOptimizeOutputShape.width; height = subPixelOptimizeOutputShape.height; subPixelOptimizeOutputShape.r = shape.r; shape = subPixelOptimizeOutputShape; } else { x = shape.x; y = shape.y; width = shape.width; height = shape.height; } if (!shape.r) { ctx.rect(x, y, width, height); } else { buildPath(ctx, shape); } ctx.closePath(); return; } }); /** * 直线 * @module zrender/graphic/shape/Line */ // Avoid create repeatly. var subPixelOptimizeOutputShape$1 = {}; var Line = Path.extend({ type: 'line', shape: { // Start point x1: 0, y1: 0, // End point x2: 0, y2: 0, percent: 1 }, style: { stroke: '#000', fill: null }, buildPath: function (ctx, shape) { var x1; var y1; var x2; var y2; if (this.subPixelOptimize) { subPixelOptimizeLine$1(subPixelOptimizeOutputShape$1, shape, this.style); x1 = subPixelOptimizeOutputShape$1.x1; y1 = subPixelOptimizeOutputShape$1.y1; x2 = subPixelOptimizeOutputShape$1.x2; y2 = subPixelOptimizeOutputShape$1.y2; } else { x1 = shape.x1; y1 = shape.y1; x2 = shape.x2; y2 = shape.y2; } var percent = shape.percent; if (percent === 0) { return; } ctx.moveTo(x1, y1); if (percent < 1) { x2 = x1 * (1 - percent) + x2 * percent; y2 = y1 * (1 - percent) + y2 * percent; } ctx.lineTo(x2, y2); }, /** * Get point at percent * @param {number} percent * @return {Array.} */ pointAt: function (p) { var shape = this.shape; return [ shape.x1 * (1 - p) + shape.x2 * p, shape.y1 * (1 - p) + shape.y2 * p ]; } }); /** * 贝塞尔曲线 * @module zrender/shape/BezierCurve */ var out = []; function someVectorAt(shape, t, isTangent) { var cpx2 = shape.cpx2; var cpy2 = shape.cpy2; if (cpx2 === null || cpy2 === null) { return [ (isTangent ? cubicDerivativeAt : cubicAt)(shape.x1, shape.cpx1, shape.cpx2, shape.x2, t), (isTangent ? cubicDerivativeAt : cubicAt)(shape.y1, shape.cpy1, shape.cpy2, shape.y2, t) ]; } else { return [ (isTangent ? quadraticDerivativeAt : quadraticAt)(shape.x1, shape.cpx1, shape.x2, t), (isTangent ? quadraticDerivativeAt : quadraticAt)(shape.y1, shape.cpy1, shape.y2, t) ]; } } var BezierCurve = Path.extend({ type: 'bezier-curve', shape: { x1: 0, y1: 0, x2: 0, y2: 0, cpx1: 0, cpy1: 0, // cpx2: 0, // cpy2: 0 // Curve show percent, for animating percent: 1 }, style: { stroke: '#000', fill: null }, buildPath: function (ctx, shape) { var x1 = shape.x1; var y1 = shape.y1; var x2 = shape.x2; var y2 = shape.y2; var cpx1 = shape.cpx1; var cpy1 = shape.cpy1; var cpx2 = shape.cpx2; var cpy2 = shape.cpy2; var percent = shape.percent; if (percent === 0) { return; } ctx.moveTo(x1, y1); if (cpx2 == null || cpy2 == null) { if (percent < 1) { quadraticSubdivide( x1, cpx1, x2, percent, out ); cpx1 = out[1]; x2 = out[2]; quadraticSubdivide( y1, cpy1, y2, percent, out ); cpy1 = out[1]; y2 = out[2]; } ctx.quadraticCurveTo( cpx1, cpy1, x2, y2 ); } else { if (percent < 1) { cubicSubdivide( x1, cpx1, cpx2, x2, percent, out ); cpx1 = out[1]; cpx2 = out[2]; x2 = out[3]; cubicSubdivide( y1, cpy1, cpy2, y2, percent, out ); cpy1 = out[1]; cpy2 = out[2]; y2 = out[3]; } ctx.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, x2, y2 ); } }, /** * Get point at percent * @param {number} t * @return {Array.} */ pointAt: function (t) { return someVectorAt(this.shape, t, false); }, /** * Get tangent at percent * @param {number} t * @return {Array.} */ tangentAt: function (t) { var p = someVectorAt(this.shape, t, true); return normalize(p, p); } }); /** * 圆弧 * @module zrender/graphic/shape/Arc */ var Arc = Path.extend({ type: 'arc', shape: { cx: 0, cy: 0, r: 0, startAngle: 0, endAngle: Math.PI * 2, clockwise: true }, style: { stroke: '#000', fill: null }, buildPath: function (ctx, shape) { var x = shape.cx; var y = shape.cy; var r = Math.max(shape.r, 0); var startAngle = shape.startAngle; var endAngle = shape.endAngle; var clockwise = shape.clockwise; var unitX = Math.cos(startAngle); var unitY = Math.sin(startAngle); ctx.moveTo(unitX * r + x, unitY * r + y); ctx.arc(x, y, r, startAngle, endAngle, !clockwise); } }); // CompoundPath to improve performance var CompoundPath = Path.extend({ type: 'compound', shape: { paths: null }, _updatePathDirty: function () { var dirtyPath = this.__dirtyPath; var paths = this.shape.paths; for (var i = 0; i < paths.length; i++) { // Mark as dirty if any subpath is dirty dirtyPath = dirtyPath || paths[i].__dirtyPath; } this.__dirtyPath = dirtyPath; this.__dirty = this.__dirty || dirtyPath; }, beforeBrush: function () { this._updatePathDirty(); var paths = this.shape.paths || []; var scale = this.getGlobalScale(); // Update path scale for (var i = 0; i < paths.length; i++) { if (!paths[i].path) { paths[i].createPathProxy(); } paths[i].path.setScale(scale[0], scale[1], paths[i].segmentIgnoreThreshold); } }, buildPath: function (ctx, shape) { var paths = shape.paths || []; for (var i = 0; i < paths.length; i++) { paths[i].buildPath(ctx, paths[i].shape, true); } }, afterBrush: function () { var paths = this.shape.paths || []; for (var i = 0; i < paths.length; i++) { paths[i].__dirtyPath = false; } }, getBoundingRect: function () { this._updatePathDirty(); return Path.prototype.getBoundingRect.call(this); } }); /** * @param {Array.} colorStops */ var Gradient = function (colorStops) { this.colorStops = colorStops || []; }; Gradient.prototype = { constructor: Gradient, addColorStop: function (offset, color) { this.colorStops.push({ offset: offset, color: color }); } }; /** * x, y, x2, y2 are all percent from 0 to 1 * @param {number} [x=0] * @param {number} [y=0] * @param {number} [x2=1] * @param {number} [y2=0] * @param {Array.} colorStops * @param {boolean} [globalCoord=false] */ var LinearGradient = function (x, y, x2, y2, colorStops, globalCoord) { // Should do nothing more in this constructor. Because gradient can be // declard by `color: {type: 'linear', colorStops: ...}`, where // this constructor will not be called. this.x = x == null ? 0 : x; this.y = y == null ? 0 : y; this.x2 = x2 == null ? 1 : x2; this.y2 = y2 == null ? 0 : y2; // Can be cloned this.type = 'linear'; // If use global coord this.global = globalCoord || false; Gradient.call(this, colorStops); }; LinearGradient.prototype = { constructor: LinearGradient }; inherits(LinearGradient, Gradient); /** * x, y, r are all percent from 0 to 1 * @param {number} [x=0.5] * @param {number} [y=0.5] * @param {number} [r=0.5] * @param {Array.} [colorStops] * @param {boolean} [globalCoord=false] */ var RadialGradient = function (x, y, r, colorStops, globalCoord) { // Should do nothing more in this constructor. Because gradient can be // declard by `color: {type: 'radial', colorStops: ...}`, where // this constructor will not be called. this.x = x == null ? 0.5 : x; this.y = y == null ? 0.5 : y; this.r = r == null ? 0.5 : r; // Can be cloned this.type = 'radial'; // If use global coord this.global = globalCoord || false; Gradient.call(this, colorStops); }; RadialGradient.prototype = { constructor: RadialGradient }; inherits(RadialGradient, Gradient); /** * Displayable for incremental rendering. It will be rendered in a separate layer * IncrementalDisplay have two main methods. `clearDisplayables` and `addDisplayables` * addDisplayables will render the added displayables incremetally. * * It use a not clearFlag to tell the painter don't clear the layer if it's the first element. */ // TODO Style override ? function IncrementalDisplayble(opts) { Displayable.call(this, opts); this._displayables = []; this._temporaryDisplayables = []; this._cursor = 0; this.notClear = true; } IncrementalDisplayble.prototype.incremental = true; IncrementalDisplayble.prototype.clearDisplaybles = function () { this._displayables = []; this._temporaryDisplayables = []; this._cursor = 0; this.dirty(); this.notClear = false; }; IncrementalDisplayble.prototype.addDisplayable = function (displayable, notPersistent) { if (notPersistent) { this._temporaryDisplayables.push(displayable); } else { this._displayables.push(displayable); } this.dirty(); }; IncrementalDisplayble.prototype.addDisplayables = function (displayables, notPersistent) { notPersistent = notPersistent || false; for (var i = 0; i < displayables.length; i++) { this.addDisplayable(displayables[i], notPersistent); } }; IncrementalDisplayble.prototype.eachPendingDisplayable = function (cb) { for (var i = this._cursor; i < this._displayables.length; i++) { cb && cb(this._displayables[i]); } for (var i = 0; i < this._temporaryDisplayables.length; i++) { cb && cb(this._temporaryDisplayables[i]); } }; IncrementalDisplayble.prototype.update = function () { this.updateTransform(); for (var i = this._cursor; i < this._displayables.length; i++) { var displayable = this._displayables[i]; // PENDING displayable.parent = this; displayable.update(); displayable.parent = null; } for (var i = 0; i < this._temporaryDisplayables.length; i++) { var displayable = this._temporaryDisplayables[i]; // PENDING displayable.parent = this; displayable.update(); displayable.parent = null; } }; IncrementalDisplayble.prototype.brush = function (ctx, prevEl) { // Render persistant displayables. for (var i = this._cursor; i < this._displayables.length; i++) { var displayable = this._displayables[i]; displayable.beforeBrush && displayable.beforeBrush(ctx); displayable.brush(ctx, i === this._cursor ? null : this._displayables[i - 1]); displayable.afterBrush && displayable.afterBrush(ctx); } this._cursor = i; // Render temporary displayables. for (var i = 0; i < this._temporaryDisplayables.length; i++) { var displayable = this._temporaryDisplayables[i]; displayable.beforeBrush && displayable.beforeBrush(ctx); displayable.brush(ctx, i === 0 ? null : this._temporaryDisplayables[i - 1]); displayable.afterBrush && displayable.afterBrush(ctx); } this._temporaryDisplayables = []; this.notClear = true; }; var m = []; IncrementalDisplayble.prototype.getBoundingRect = function () { if (!this._rect) { var rect = new BoundingRect(Infinity, Infinity, -Infinity, -Infinity); for (var i = 0; i < this._displayables.length; i++) { var displayable = this._displayables[i]; var childRect = displayable.getBoundingRect().clone(); if (displayable.needLocalTransform()) { childRect.applyTransform(displayable.getLocalTransform(m)); } rect.union(childRect); } this._rect = rect; } return this._rect; }; IncrementalDisplayble.prototype.contain = function (x, y) { var localPos = this.transformCoordToLocal(x, y); var rect = this.getBoundingRect(); if (rect.contain(localPos[0], localPos[1])) { for (var i = 0; i < this._displayables.length; i++) { var displayable = this._displayables[i]; if (displayable.contain(x, y)) { return true; } } } return false; }; inherits(IncrementalDisplayble, Displayable); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var mathMax$1 = Math.max; var mathMin$1 = Math.min; var EMPTY_OBJ = {}; var Z2_EMPHASIS_LIFT = 1; // key: label model property nane, value: style property name. var CACHED_LABEL_STYLE_PROPERTIES = { color: 'textFill', textBorderColor: 'textStroke', textBorderWidth: 'textStrokeWidth' }; var EMPHASIS = 'emphasis'; var NORMAL = 'normal'; // Reserve 0 as default. var _highlightNextDigit = 1; var _highlightKeyMap = {}; var _customShapeMap = {}; /** * Extend shape with parameters */ function extendShape(opts) { return Path.extend(opts); } /** * Extend path */ function extendPath(pathData, opts) { return extendFromString(pathData, opts); } /** * Register a user defined shape. * The shape class can be fetched by `getShapeClass` * This method will overwrite the registered shapes, including * the registered built-in shapes, if using the same `name`. * The shape can be used in `custom series` and * `graphic component` by declaring `{type: name}`. * * @param {string} name * @param {Object} ShapeClass Can be generated by `extendShape`. */ function registerShape(name, ShapeClass) { _customShapeMap[name] = ShapeClass; } /** * Find shape class registered by `registerShape`. Usually used in * fetching user defined shape. * * [Caution]: * (1) This method **MUST NOT be used inside echarts !!!**, unless it is prepared * to use user registered shapes. * Because the built-in shape (see `getBuiltInShape`) will be registered by * `registerShape` by default. That enables users to get both built-in * shapes as well as the shapes belonging to themsleves. But users can overwrite * the built-in shapes by using names like 'circle', 'rect' via calling * `registerShape`. So the echarts inner featrues should not fetch shapes from here * in case that it is overwritten by users, except that some features, like * `custom series`, `graphic component`, do it deliberately. * * (2) In the features like `custom series`, `graphic component`, the user input * `{tpye: 'xxx'}` does not only specify shapes but also specify other graphic * elements like `'group'`, `'text'`, `'image'` or event `'path'`. Those names * are reserved names, that is, if some user register a shape named `'image'`, * the shape will not be used. If we intending to add some more reserved names * in feature, that might bring break changes (disable some existing user shape * names). But that case probably rearly happen. So we dont make more mechanism * to resolve this issue here. * * @param {string} name * @return {Object} The shape class. If not found, return nothing. */ function getShapeClass(name) { if (_customShapeMap.hasOwnProperty(name)) { return _customShapeMap[name]; } } /** * Create a path element from path data string * @param {string} pathData * @param {Object} opts * @param {module:zrender/core/BoundingRect} rect * @param {string} [layout=cover] 'center' or 'cover' */ function makePath(pathData, opts, rect, layout) { var path = createFromString(pathData, opts); if (rect) { if (layout === 'center') { rect = centerGraphic(rect, path.getBoundingRect()); } resizePath(path, rect); } return path; } /** * Create a image element from image url * @param {string} imageUrl image url * @param {Object} opts options * @param {module:zrender/core/BoundingRect} rect constrain rect * @param {string} [layout=cover] 'center' or 'cover' */ function makeImage(imageUrl, rect, layout) { var path = new ZImage({ style: { image: imageUrl, x: rect.x, y: rect.y, width: rect.width, height: rect.height }, onload: function (img) { if (layout === 'center') { var boundingRect = { width: img.width, height: img.height }; path.setStyle(centerGraphic(rect, boundingRect)); } } }); return path; } /** * Get position of centered element in bounding box. * * @param {Object} rect element local bounding box * @param {Object} boundingRect constraint bounding box * @return {Object} element position containing x, y, width, and height */ function centerGraphic(rect, boundingRect) { // Set rect to center, keep width / height ratio. var aspect = boundingRect.width / boundingRect.height; var width = rect.height * aspect; var height; if (width <= rect.width) { height = rect.height; } else { width = rect.width; height = width / aspect; } var cx = rect.x + rect.width / 2; var cy = rect.y + rect.height / 2; return { x: cx - width / 2, y: cy - height / 2, width: width, height: height }; } var mergePath = mergePath$1; /** * Resize a path to fit the rect * @param {module:zrender/graphic/Path} path * @param {Object} rect */ function resizePath(path, rect) { if (!path.applyTransform) { return; } var pathRect = path.getBoundingRect(); var m = pathRect.calculateTransform(rect); path.applyTransform(m); } /** * Sub pixel optimize line for canvas * * @param {Object} param * @param {Object} [param.shape] * @param {number} [param.shape.x1] * @param {number} [param.shape.y1] * @param {number} [param.shape.x2] * @param {number} [param.shape.y2] * @param {Object} [param.style] * @param {number} [param.style.lineWidth] * @return {Object} Modified param */ function subPixelOptimizeLine(param) { subPixelOptimizeLine$1(param.shape, param.shape, param.style); return param; } /** * Sub pixel optimize rect for canvas * * @param {Object} param * @param {Object} [param.shape] * @param {number} [param.shape.x] * @param {number} [param.shape.y] * @param {number} [param.shape.width] * @param {number} [param.shape.height] * @param {Object} [param.style] * @param {number} [param.style.lineWidth] * @return {Object} Modified param */ function subPixelOptimizeRect(param) { subPixelOptimizeRect$1(param.shape, param.shape, param.style); return param; } /** * Sub pixel optimize for canvas * * @param {number} position Coordinate, such as x, y * @param {number} lineWidth Should be nonnegative integer. * @param {boolean=} positiveOrNegative Default false (negative). * @return {number} Optimized position. */ var subPixelOptimize = subPixelOptimize$1; function hasFillOrStroke(fillOrStroke) { return fillOrStroke != null && fillOrStroke !== 'none'; } // Most lifted color are duplicated. var liftedColorMap = createHashMap(); var liftedColorCount = 0; function liftColor(color) { if (typeof color !== 'string') { return color; } var liftedColor = liftedColorMap.get(color); if (!liftedColor) { liftedColor = lift(color, -0.1); if (liftedColorCount < 10000) { liftedColorMap.set(color, liftedColor); liftedColorCount++; } } return liftedColor; } function cacheElementStl(el) { if (!el.__hoverStlDirty) { return; } el.__hoverStlDirty = false; var hoverStyle = el.__hoverStl; if (!hoverStyle) { el.__cachedNormalStl = el.__cachedNormalZ2 = null; return; } var normalStyle = el.__cachedNormalStl = {}; el.__cachedNormalZ2 = el.z2; var elStyle = el.style; for (var name in hoverStyle) { // See comment in `singleEnterEmphasis`. if (hoverStyle[name] != null) { normalStyle[name] = elStyle[name]; } } // Always cache fill and stroke to normalStyle for lifting color. normalStyle.fill = elStyle.fill; normalStyle.stroke = elStyle.stroke; } function singleEnterEmphasis(el) { var hoverStl = el.__hoverStl; if (!hoverStl || el.__highlighted) { return; } var zr = el.__zr; var useHoverLayer = el.useHoverLayer && zr && zr.painter.type === 'canvas'; el.__highlighted = useHoverLayer ? 'layer' : 'plain'; if (el.isGroup || (!zr && el.useHoverLayer)) { return; } var elTarget = el; var targetStyle = el.style; if (useHoverLayer) { elTarget = zr.addHover(el); targetStyle = elTarget.style; } rollbackDefaultTextStyle(targetStyle); if (!useHoverLayer) { cacheElementStl(elTarget); } // styles can be: // { // label: { // show: false, // position: 'outside', // fontSize: 18 // }, // emphasis: { // label: { // show: true // } // } // }, // where properties of `emphasis` may not appear in `normal`. We previously use // module:echarts/util/model#defaultEmphasis to merge `normal` to `emphasis`. // But consider rich text and setOption in merge mode, it is impossible to cover // all properties in merge. So we use merge mode when setting style here. // But we choose the merge strategy that only properties that is not `null/undefined`. // Because when making a textStyle (espacially rich text), it is not easy to distinguish // `hasOwnProperty` and `null/undefined` in code, so we trade them as the same for simplicity. // But this strategy brings a trouble that `null/undefined` can not be used to remove // style any more in `emphasis`. Users can both set properties directly on normal and // emphasis to avoid this issue, or we might support `'none'` for this case if required. targetStyle.extendFrom(hoverStl); setDefaultHoverFillStroke(targetStyle, hoverStl, 'fill'); setDefaultHoverFillStroke(targetStyle, hoverStl, 'stroke'); applyDefaultTextStyle(targetStyle); if (!useHoverLayer) { el.dirty(false); el.z2 += Z2_EMPHASIS_LIFT; } } function setDefaultHoverFillStroke(targetStyle, hoverStyle, prop) { if (!hasFillOrStroke(hoverStyle[prop]) && hasFillOrStroke(targetStyle[prop])) { targetStyle[prop] = liftColor(targetStyle[prop]); } } function singleEnterNormal(el) { var highlighted = el.__highlighted; if (!highlighted) { return; } el.__highlighted = false; if (el.isGroup) { return; } if (highlighted === 'layer') { el.__zr && el.__zr.removeHover(el); } else { var style = el.style; var normalStl = el.__cachedNormalStl; if (normalStl) { rollbackDefaultTextStyle(style); el.setStyle(normalStl); applyDefaultTextStyle(style); } // `__cachedNormalZ2` will not be reset if calling `setElementHoverStyle` // when `el` is on emphasis state. So here by comparing with 1, we try // hard to make the bug case rare. var normalZ2 = el.__cachedNormalZ2; if (normalZ2 != null && el.z2 - normalZ2 === Z2_EMPHASIS_LIFT) { el.z2 = normalZ2; } } } function traverseUpdate(el, updater, commonParam) { // If root is group, also enter updater for `highDownOnUpdate`. var fromState = NORMAL; var toState = NORMAL; var trigger; // See the rule of `highDownOnUpdate` on `graphic.setAsHighDownDispatcher`. el.__highlighted && (fromState = EMPHASIS, trigger = true); updater(el, commonParam); el.__highlighted && (toState = EMPHASIS, trigger = true); el.isGroup && el.traverse(function (child) { !child.isGroup && updater(child, commonParam); }); trigger && el.__highDownOnUpdate && el.__highDownOnUpdate(fromState, toState); } /** * Set hover style (namely "emphasis style") of element, based on the current * style of the given `el`. * This method should be called after all of the normal styles have been adopted * to the `el`. See the reason on `setHoverStyle`. * * @param {module:zrender/Element} el Should not be `zrender/container/Group`. * @param {Object} [el.hoverStyle] Can be set on el or its descendants, * e.g., `el.hoverStyle = ...; graphic.setHoverStyle(el); `. * Often used when item group has a label element and it's hoverStyle is different. * @param {Object|boolean} [hoverStl] The specified hover style. * If set as `false`, disable the hover style. * Similarly, The `el.hoverStyle` can alse be set * as `false` to disable the hover style. * Otherwise, use the default hover style if not provided. */ function setElementHoverStyle(el, hoverStl) { // For performance consideration, it might be better to make the "hover style" only the // difference properties from the "normal style", but not a entire copy of all styles. hoverStl = el.__hoverStl = hoverStl !== false && (el.hoverStyle || hoverStl || {}); el.__hoverStlDirty = true; // FIXME // It is not completely right to save "normal"/"emphasis" flag on elements. // It probably should be saved on `data` of series. Consider the cases: // (1) A highlighted elements are moved out of the view port and re-enter // again by dataZoom. // (2) call `setOption` and replace elements totally when they are highlighted. if (el.__highlighted) { // Consider the case: // The styles of a highlighted `el` is being updated. The new "emphasis style" // should be adapted to the `el`. Notice here new "normal styles" should have // been set outside and the cached "normal style" is out of date. el.__cachedNormalStl = null; // Do not clear `__cachedNormalZ2` here, because setting `z2` is not a constraint // of this method. In most cases, `z2` is not set and hover style should be able // to rollback. Of course, that would bring bug, but only in a rare case, see // `doSingleLeaveHover` for details. singleEnterNormal(el); singleEnterEmphasis(el); } } function onElementMouseOver(e) { !shouldSilent(this, e) // "emphasis" event highlight has higher priority than mouse highlight. && !this.__highByOuter && traverseUpdate(this, singleEnterEmphasis); } function onElementMouseOut(e) { !shouldSilent(this, e) // "emphasis" event highlight has higher priority than mouse highlight. && !this.__highByOuter && traverseUpdate(this, singleEnterNormal); } function onElementEmphasisEvent(highlightDigit) { this.__highByOuter |= 1 << (highlightDigit || 0); traverseUpdate(this, singleEnterEmphasis); } function onElementNormalEvent(highlightDigit) { !(this.__highByOuter &= ~(1 << (highlightDigit || 0))) && traverseUpdate(this, singleEnterNormal); } function shouldSilent(el, e) { return el.__highDownSilentOnTouch && e.zrByTouch; } /** * Set hover style (namely "emphasis style") of element, * based on the current style of the given `el`. * * (1) * **CONSTRAINTS** for this method: * This method MUST be called after all of the normal styles having been adopted * to the `el`. * The input `hoverStyle` (that is, "emphasis style") MUST be the subset of the * "normal style" having been set to the el. * `color` MUST be one of the "normal styles" (because color might be lifted as * a default hover style). * * The reason: this method treat the current style of the `el` as the "normal style" * and cache them when enter/update the "emphasis style". Consider the case: the `el` * is in "emphasis" state and `setOption`/`dispatchAction` trigger the style updating * logic, where the el should shift from the original emphasis style to the new * "emphasis style" and should be able to "downplay" back to the new "normal style". * * Indeed, it is error-prone to make a interface has so many constraints, but I have * not found a better solution yet to fit the backward compatibility, performance and * the current programming style. * * (2) * Call the method for a "root" element once. Do not call it for each descendants. * If the descendants elemenets of a group has itself hover style different from the * root group, we can simply mount the style on `el.hoverStyle` for them, but should * not call this method for them. * * (3) These input parameters can be set directly on `el`: * * @param {module:zrender/Element} el * @param {Object} [el.hoverStyle] See `graphic.setElementHoverStyle`. * @param {boolean} [el.highDownSilentOnTouch=false] See `graphic.setAsHighDownDispatcher`. * @param {Function} [el.highDownOnUpdate] See `graphic.setAsHighDownDispatcher`. * @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`. */ function setHoverStyle(el, hoverStyle) { setAsHighDownDispatcher(el, true); traverseUpdate(el, setElementHoverStyle, hoverStyle); } /** * @param {module:zrender/Element} el * @param {Function} [el.highDownOnUpdate] Called when state updated. * Since `setHoverStyle` has the constraint that it must be called after * all of the normal style updated, `highDownOnUpdate` is not needed to * trigger if both `fromState` and `toState` is 'normal', and needed to * trigger if both `fromState` and `toState` is 'emphasis', which enables * to sync outside style settings to "emphasis" state. * @this {string} This dispatcher `el`. * @param {string} fromState Can be "normal" or "emphasis". * `fromState` might equal to `toState`, * for example, when this method is called when `el` is * on "emphasis" state. * @param {string} toState Can be "normal" or "emphasis". * * FIXME * CAUTION: Do not expose `highDownOnUpdate` outside echarts. * Because it is not a complete solution. The update * listener should not have been mount in element, * and the normal/emphasis state should not have * mantained on elements. * * @param {boolean} [el.highDownSilentOnTouch=false] * In touch device, mouseover event will be trigger on touchstart event * (see module:zrender/dom/HandlerProxy). By this mechanism, we can * conveniently use hoverStyle when tap on touch screen without additional * code for compatibility. * But if the chart/component has select feature, which usually also use * hoverStyle, there might be conflict between 'select-highlight' and * 'hover-highlight' especially when roam is enabled (see geo for example). * In this case, `highDownSilentOnTouch` should be used to disable * hover-highlight on touch device. * @param {boolean} [asDispatcher=true] If `false`, do not set as "highDownDispatcher". */ function setAsHighDownDispatcher(el, asDispatcher) { var disable = asDispatcher === false; // Make `highDownSilentOnTouch` and `highDownOnUpdate` only work after // `setAsHighDownDispatcher` called. Avoid it is modified by user unexpectedly. el.__highDownSilentOnTouch = el.highDownSilentOnTouch; el.__highDownOnUpdate = el.highDownOnUpdate; // Simple optimize, since this method might be // called for each elements of a group in some cases. if (!disable || el.__highDownDispatcher) { var method = disable ? 'off' : 'on'; // Duplicated function will be auto-ignored, see Eventful.js. el[method]('mouseover', onElementMouseOver)[method]('mouseout', onElementMouseOut); // Emphasis, normal can be triggered manually by API or other components like hover link. el[method]('emphasis', onElementEmphasisEvent)[method]('normal', onElementNormalEvent); // Also keep previous record. el.__highByOuter = el.__highByOuter || 0; el.__highDownDispatcher = !disable; } } /** * @param {module:zrender/src/Element} el * @return {boolean} */ function isHighDownDispatcher(el) { return !!(el && el.__highDownDispatcher); } /** * Support hightlight/downplay record on each elements. * For the case: hover highlight/downplay (legend, visualMap, ...) and * user triggerred hightlight/downplay should not conflict. * Only all of the highlightDigit cleared, return to normal. * @param {string} highlightKey * @return {number} highlightDigit */ function getHighlightDigit(highlightKey) { var highlightDigit = _highlightKeyMap[highlightKey]; if (highlightDigit == null && _highlightNextDigit <= 32) { highlightDigit = _highlightKeyMap[highlightKey] = _highlightNextDigit++; } return highlightDigit; } /** * See more info in `setTextStyleCommon`. * @param {Object|module:zrender/graphic/Style} normalStyle * @param {Object} emphasisStyle * @param {module:echarts/model/Model} normalModel * @param {module:echarts/model/Model} emphasisModel * @param {Object} opt Check `opt` of `setTextStyleCommon` to find other props. * @param {string|Function} [opt.defaultText] * @param {module:echarts/model/Model} [opt.labelFetcher] Fetch text by * `opt.labelFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)` * @param {number} [opt.labelDataIndex] Fetch text by * `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)` * @param {number} [opt.labelDimIndex] Fetch text by * `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)` * @param {string} [opt.labelProp] Fetch text by * `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)` * @param {Object} [normalSpecified] * @param {Object} [emphasisSpecified] */ function setLabelStyle( normalStyle, emphasisStyle, normalModel, emphasisModel, opt, normalSpecified, emphasisSpecified ) { opt = opt || EMPTY_OBJ; var labelFetcher = opt.labelFetcher; var labelDataIndex = opt.labelDataIndex; var labelDimIndex = opt.labelDimIndex; var labelProp = opt.labelProp; // This scenario, `label.normal.show = true; label.emphasis.show = false`, // is not supported util someone requests. var showNormal = normalModel.getShallow('show'); var showEmphasis = emphasisModel.getShallow('show'); // Consider performance, only fetch label when necessary. // If `normal.show` is `false` and `emphasis.show` is `true` and `emphasis.formatter` is not set, // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`. var baseText; if (showNormal || showEmphasis) { if (labelFetcher) { baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex, labelProp); } if (baseText == null) { baseText = isFunction$1(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText; } } var normalStyleText = showNormal ? baseText : null; var emphasisStyleText = showEmphasis ? retrieve2( labelFetcher ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex, labelProp) : null, baseText ) : null; // Optimize: If style.text is null, text will not be drawn. if (normalStyleText != null || emphasisStyleText != null) { // Always set `textStyle` even if `normalStyle.text` is null, because default // values have to be set on `normalStyle`. // If we set default values on `emphasisStyle`, consider case: // Firstly, `setOption(... label: {normal: {text: null}, emphasis: {show: true}} ...);` // Secondly, `setOption(... label: {noraml: {show: true, text: 'abc', color: 'red'} ...);` // Then the 'red' will not work on emphasis. setTextStyle(normalStyle, normalModel, normalSpecified, opt); setTextStyle(emphasisStyle, emphasisModel, emphasisSpecified, opt, true); } normalStyle.text = normalStyleText; emphasisStyle.text = emphasisStyleText; } /** * Modify label style manually. * Only works after `setLabelStyle` and `setElementHoverStyle` called. * * @param {module:zrender/src/Element} el * @param {Object} [normalStyleProps] optional * @param {Object} [emphasisStyleProps] optional */ function modifyLabelStyle(el, normalStyleProps, emphasisStyleProps) { var elStyle = el.style; if (normalStyleProps) { rollbackDefaultTextStyle(elStyle); el.setStyle(normalStyleProps); applyDefaultTextStyle(elStyle); } elStyle = el.__hoverStl; if (emphasisStyleProps && elStyle) { rollbackDefaultTextStyle(elStyle); extend(elStyle, emphasisStyleProps); applyDefaultTextStyle(elStyle); } } /** * Set basic textStyle properties. * See more info in `setTextStyleCommon`. * @param {Object|module:zrender/graphic/Style} textStyle * @param {module:echarts/model/Model} model * @param {Object} [specifiedTextStyle] Can be overrided by settings in model. * @param {Object} [opt] See `opt` of `setTextStyleCommon`. * @param {boolean} [isEmphasis] */ function setTextStyle( textStyle, textStyleModel, specifiedTextStyle, opt, isEmphasis ) { setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis); specifiedTextStyle && extend(textStyle, specifiedTextStyle); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false); return textStyle; } /** * Set text option in the style. * See more info in `setTextStyleCommon`. * @deprecated * @param {Object} textStyle * @param {module:echarts/model/Model} labelModel * @param {string|boolean} defaultColor Default text color. * If set as false, it will be processed as a emphasis style. */ function setText(textStyle, labelModel, defaultColor) { var opt = {isRectText: true}; var isEmphasis; if (defaultColor === false) { isEmphasis = true; } else { // Support setting color as 'auto' to get visual color. opt.autoColor = defaultColor; } setTextStyleCommon(textStyle, labelModel, opt, isEmphasis); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false); } /** * The uniform entry of set text style, that is, retrieve style definitions * from `model` and set to `textStyle` object. * * Never in merge mode, but in overwrite mode, that is, all of the text style * properties will be set. (Consider the states of normal and emphasis and * default value can be adopted, merge would make the logic too complicated * to manage.) * * The `textStyle` object can either be a plain object or an instance of * `zrender/src/graphic/Style`, and either be the style of normal or emphasis. * After this mothod called, the `textStyle` object can then be used in * `el.setStyle(textStyle)` or `el.hoverStyle = textStyle`. * * Default value will be adopted and `insideRollbackOpt` will be created. * See `applyDefaultTextStyle` `rollbackDefaultTextStyle` for more details. * * opt: { * disableBox: boolean, Whether diable drawing box of block (outer most). * isRectText: boolean, * autoColor: string, specify a color when color is 'auto', * for textFill, textStroke, textBackgroundColor, and textBorderColor. * If autoColor specified, it is used as default textFill. * useInsideStyle: * `true`: Use inside style (textFill, textStroke, textStrokeWidth) * if `textFill` is not specified. * `false`: Do not use inside style. * `null/undefined`: use inside style if `isRectText` is true and * `textFill` is not specified and textPosition contains `'inside'`. * forceRich: boolean * } */ function setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis) { // Consider there will be abnormal when merge hover style to normal style if given default value. opt = opt || EMPTY_OBJ; if (opt.isRectText) { var textPosition; if (opt.getTextPosition) { textPosition = opt.getTextPosition(textStyleModel, isEmphasis); } else { textPosition = textStyleModel.getShallow('position') || (isEmphasis ? null : 'inside'); // 'outside' is not a valid zr textPostion value, but used // in bar series, and magric type should be considered. textPosition === 'outside' && (textPosition = 'top'); } textStyle.textPosition = textPosition; textStyle.textOffset = textStyleModel.getShallow('offset'); var labelRotate = textStyleModel.getShallow('rotate'); labelRotate != null && (labelRotate *= Math.PI / 180); textStyle.textRotation = labelRotate; textStyle.textDistance = retrieve2( textStyleModel.getShallow('distance'), isEmphasis ? null : 5 ); } var ecModel = textStyleModel.ecModel; var globalTextStyle = ecModel && ecModel.option.textStyle; // Consider case: // { // data: [{ // value: 12, // label: { // rich: { // // no 'a' here but using parent 'a'. // } // } // }], // rich: { // a: { ... } // } // } var richItemNames = getRichItemNames(textStyleModel); var richResult; if (richItemNames) { richResult = {}; for (var name in richItemNames) { if (richItemNames.hasOwnProperty(name)) { // Cascade is supported in rich. var richTextStyle = textStyleModel.getModel(['rich', name]); // In rich, never `disableBox`. // FIXME: consider `label: {formatter: '{a|xx}', color: 'blue', rich: {a: {}}}`, // the default color `'blue'` will not be adopted if no color declared in `rich`. // That might confuses users. So probably we should put `textStyleModel` as the // root ancestor of the `richTextStyle`. But that would be a break change. setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isEmphasis); } } } textStyle.rich = richResult; setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, true); if (opt.forceRich && !opt.textStyle) { opt.textStyle = {}; } return textStyle; } // Consider case: // { // data: [{ // value: 12, // label: { // rich: { // // no 'a' here but using parent 'a'. // } // } // }], // rich: { // a: { ... } // } // } function getRichItemNames(textStyleModel) { // Use object to remove duplicated names. var richItemNameMap; while (textStyleModel && textStyleModel !== textStyleModel.ecModel) { var rich = (textStyleModel.option || EMPTY_OBJ).rich; if (rich) { richItemNameMap = richItemNameMap || {}; for (var name in rich) { if (rich.hasOwnProperty(name)) { richItemNameMap[name] = 1; } } } textStyleModel = textStyleModel.parentModel; } return richItemNameMap; } function setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isBlock) { // In merge mode, default value should not be given. globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ; textStyle.textFill = getAutoColor(textStyleModel.getShallow('color'), opt) || globalTextStyle.color; textStyle.textStroke = getAutoColor(textStyleModel.getShallow('textBorderColor'), opt) || globalTextStyle.textBorderColor; textStyle.textStrokeWidth = retrieve2( textStyleModel.getShallow('textBorderWidth'), globalTextStyle.textBorderWidth ); if (!isEmphasis) { if (isBlock) { textStyle.insideRollbackOpt = opt; applyDefaultTextStyle(textStyle); } // Set default finally. if (textStyle.textFill == null) { textStyle.textFill = opt.autoColor; } } // Do not use `getFont` here, because merge should be supported, where // part of these properties may be changed in emphasis style, and the // others should remain their original value got from normal style. textStyle.fontStyle = textStyleModel.getShallow('fontStyle') || globalTextStyle.fontStyle; textStyle.fontWeight = textStyleModel.getShallow('fontWeight') || globalTextStyle.fontWeight; textStyle.fontSize = textStyleModel.getShallow('fontSize') || globalTextStyle.fontSize; textStyle.fontFamily = textStyleModel.getShallow('fontFamily') || globalTextStyle.fontFamily; textStyle.textAlign = textStyleModel.getShallow('align'); textStyle.textVerticalAlign = textStyleModel.getShallow('verticalAlign') || textStyleModel.getShallow('baseline'); textStyle.textLineHeight = textStyleModel.getShallow('lineHeight'); textStyle.textWidth = textStyleModel.getShallow('width'); textStyle.textHeight = textStyleModel.getShallow('height'); textStyle.textTag = textStyleModel.getShallow('tag'); if (!isBlock || !opt.disableBox) { textStyle.textBackgroundColor = getAutoColor(textStyleModel.getShallow('backgroundColor'), opt); textStyle.textPadding = textStyleModel.getShallow('padding'); textStyle.textBorderColor = getAutoColor(textStyleModel.getShallow('borderColor'), opt); textStyle.textBorderWidth = textStyleModel.getShallow('borderWidth'); textStyle.textBorderRadius = textStyleModel.getShallow('borderRadius'); textStyle.textBoxShadowColor = textStyleModel.getShallow('shadowColor'); textStyle.textBoxShadowBlur = textStyleModel.getShallow('shadowBlur'); textStyle.textBoxShadowOffsetX = textStyleModel.getShallow('shadowOffsetX'); textStyle.textBoxShadowOffsetY = textStyleModel.getShallow('shadowOffsetY'); } textStyle.textShadowColor = textStyleModel.getShallow('textShadowColor') || globalTextStyle.textShadowColor; textStyle.textShadowBlur = textStyleModel.getShallow('textShadowBlur') || globalTextStyle.textShadowBlur; textStyle.textShadowOffsetX = textStyleModel.getShallow('textShadowOffsetX') || globalTextStyle.textShadowOffsetX; textStyle.textShadowOffsetY = textStyleModel.getShallow('textShadowOffsetY') || globalTextStyle.textShadowOffsetY; } function getAutoColor(color, opt) { return color !== 'auto' ? color : (opt && opt.autoColor) ? opt.autoColor : null; } /** * Give some default value to the input `textStyle` object, based on the current settings * in this `textStyle` object. * * The Scenario: * when text position is `inside` and `textFill` is not specified, we show * text border by default for better view. But it should be considered that text position * might be changed when hovering or being emphasis, where the `insideRollback` is used to * restore the style. * * Usage (& NOTICE): * When a style object (eithor plain object or instance of `zrender/src/graphic/Style`) is * about to be modified on its text related properties, `rollbackDefaultTextStyle` should * be called before the modification and `applyDefaultTextStyle` should be called after that. * (For the case that all of the text related properties is reset, like `setTextStyleCommon` * does, `rollbackDefaultTextStyle` is not needed to be called). */ function applyDefaultTextStyle(textStyle) { var textPosition = textStyle.textPosition; var opt = textStyle.insideRollbackOpt; var insideRollback; if (opt && textStyle.textFill == null) { var autoColor = opt.autoColor; var isRectText = opt.isRectText; var useInsideStyle = opt.useInsideStyle; var useInsideStyleCache = useInsideStyle !== false && (useInsideStyle === true || (isRectText && textPosition // textPosition can be [10, 30] && typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0 ) ); var useAutoColorCache = !useInsideStyleCache && autoColor != null; // All of the props declared in `CACHED_LABEL_STYLE_PROPERTIES` are to be cached. if (useInsideStyleCache || useAutoColorCache) { insideRollback = { textFill: textStyle.textFill, textStroke: textStyle.textStroke, textStrokeWidth: textStyle.textStrokeWidth }; } if (useInsideStyleCache) { textStyle.textFill = '#fff'; // Consider text with #fff overflow its container. if (textStyle.textStroke == null) { textStyle.textStroke = autoColor; textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2); } } if (useAutoColorCache) { textStyle.textFill = autoColor; } } // Always set `insideRollback`, so that the previous one can be cleared. textStyle.insideRollback = insideRollback; } /** * Consider the case: in a scatter, * label: { * normal: {position: 'inside'}, * emphasis: {position: 'top'} * } * In the normal state, the `textFill` will be set as '#fff' for pretty view (see * `applyDefaultTextStyle`), but when switching to emphasis state, the `textFill` * should be retured to 'autoColor', but not keep '#fff'. */ function rollbackDefaultTextStyle(style) { var insideRollback = style.insideRollback; if (insideRollback) { // Reset all of the props in `CACHED_LABEL_STYLE_PROPERTIES`. style.textFill = insideRollback.textFill; style.textStroke = insideRollback.textStroke; style.textStrokeWidth = insideRollback.textStrokeWidth; style.insideRollback = null; } } function getFont(opt, ecModel) { var gTextStyleModel = ecModel && ecModel.getModel('textStyle'); return trim([ // FIXME in node-canvas fontWeight is before fontStyle opt.fontStyle || gTextStyleModel && gTextStyleModel.getShallow('fontStyle') || '', opt.fontWeight || gTextStyleModel && gTextStyleModel.getShallow('fontWeight') || '', (opt.fontSize || gTextStyleModel && gTextStyleModel.getShallow('fontSize') || 12) + 'px', opt.fontFamily || gTextStyleModel && gTextStyleModel.getShallow('fontFamily') || 'sans-serif' ].join(' ')); } function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) { if (typeof dataIndex === 'function') { cb = dataIndex; dataIndex = null; } // Do not check 'animation' property directly here. Consider this case: // animation model is an `itemModel`, whose does not have `isAnimationEnabled` // but its parent model (`seriesModel`) does. var animationEnabled = animatableModel && animatableModel.isAnimationEnabled(); if (animationEnabled) { var postfix = isUpdate ? 'Update' : ''; var duration = animatableModel.getShallow('animationDuration' + postfix); var animationEasing = animatableModel.getShallow('animationEasing' + postfix); var animationDelay = animatableModel.getShallow('animationDelay' + postfix); if (typeof animationDelay === 'function') { animationDelay = animationDelay( dataIndex, animatableModel.getAnimationDelayParams ? animatableModel.getAnimationDelayParams(el, dataIndex) : null ); } if (typeof duration === 'function') { duration = duration(dataIndex); } duration > 0 ? el.animateTo(props, duration, animationDelay || 0, animationEasing, cb, !!cb) : (el.stopAnimation(), el.attr(props), cb && cb()); } else { el.stopAnimation(); el.attr(props); cb && cb(); } } /** * Update graphic element properties with or without animation according to the * configuration in series. * * Caution: this method will stop previous animation. * So do not use this method to one element twice before * animation starts, unless you know what you are doing. * * @param {module:zrender/Element} el * @param {Object} props * @param {module:echarts/model/Model} [animatableModel] * @param {number} [dataIndex] * @param {Function} [cb] * @example * graphic.updateProps(el, { * position: [100, 100] * }, seriesModel, dataIndex, function () { console.log('Animation done!'); }); * // Or * graphic.updateProps(el, { * position: [100, 100] * }, seriesModel, function () { console.log('Animation done!'); }); */ function updateProps(el, props, animatableModel, dataIndex, cb) { animateOrSetProps(true, el, props, animatableModel, dataIndex, cb); } /** * Init graphic element properties with or without animation according to the * configuration in series. * * Caution: this method will stop previous animation. * So do not use this method to one element twice before * animation starts, unless you know what you are doing. * * @param {module:zrender/Element} el * @param {Object} props * @param {module:echarts/model/Model} [animatableModel] * @param {number} [dataIndex] * @param {Function} cb */ function initProps(el, props, animatableModel, dataIndex, cb) { animateOrSetProps(false, el, props, animatableModel, dataIndex, cb); } /** * Get transform matrix of target (param target), * in coordinate of its ancestor (param ancestor) * * @param {module:zrender/mixin/Transformable} target * @param {module:zrender/mixin/Transformable} [ancestor] */ function getTransform(target, ancestor) { var mat = identity([]); while (target && target !== ancestor) { mul$1(mat, target.getLocalTransform(), mat); target = target.parent; } return mat; } /** * Apply transform to an vertex. * @param {Array.} target [x, y] * @param {Array.|TypedArray.|Object} transform Can be: * + Transform matrix: like [1, 0, 0, 1, 0, 0] * + {position, rotation, scale}, the same as `zrender/Transformable`. * @param {boolean=} invert Whether use invert matrix. * @return {Array.} [x, y] */ function applyTransform$1(target, transform, invert$$1) { if (transform && !isArrayLike(transform)) { transform = Transformable.getLocalTransform(transform); } if (invert$$1) { transform = invert([], transform); } return applyTransform([], target, transform); } /** * @param {string} direction 'left' 'right' 'top' 'bottom' * @param {Array.} transform Transform matrix: like [1, 0, 0, 1, 0, 0] * @param {boolean=} invert Whether use invert matrix. * @return {string} Transformed direction. 'left' 'right' 'top' 'bottom' */ function transformDirection(direction, transform, invert$$1) { // Pick a base, ensure that transform result will not be (0, 0). var hBase = (transform[4] === 0 || transform[5] === 0 || transform[0] === 0) ? 1 : Math.abs(2 * transform[4] / transform[0]); var vBase = (transform[4] === 0 || transform[5] === 0 || transform[2] === 0) ? 1 : Math.abs(2 * transform[4] / transform[2]); var vertex = [ direction === 'left' ? -hBase : direction === 'right' ? hBase : 0, direction === 'top' ? -vBase : direction === 'bottom' ? vBase : 0 ]; vertex = applyTransform$1(vertex, transform, invert$$1); return Math.abs(vertex[0]) > Math.abs(vertex[1]) ? (vertex[0] > 0 ? 'right' : 'left') : (vertex[1] > 0 ? 'bottom' : 'top'); } /** * Apply group transition animation from g1 to g2. * If no animatableModel, no animation. */ function groupTransition(g1, g2, animatableModel, cb) { if (!g1 || !g2) { return; } function getElMap(g) { var elMap = {}; g.traverse(function (el) { if (!el.isGroup && el.anid) { elMap[el.anid] = el; } }); return elMap; } function getAnimatableProps(el) { var obj = { position: clone$1(el.position), rotation: el.rotation }; if (el.shape) { obj.shape = extend({}, el.shape); } return obj; } var elMap1 = getElMap(g1); g2.traverse(function (el) { if (!el.isGroup && el.anid) { var oldEl = elMap1[el.anid]; if (oldEl) { var newProp = getAnimatableProps(el); el.attr(getAnimatableProps(oldEl)); updateProps(el, newProp, animatableModel, el.dataIndex); } // else { // if (el.previousProps) { // graphic.updateProps // } // } } }); } /** * @param {Array.>} points Like: [[23, 44], [53, 66], ...] * @param {Object} rect {x, y, width, height} * @return {Array.>} A new clipped points. */ function clipPointsByRect(points, rect) { // FIXME: this way migth be incorrect when grpahic clipped by a corner. // and when element have border. return map(points, function (point) { var x = point[0]; x = mathMax$1(x, rect.x); x = mathMin$1(x, rect.x + rect.width); var y = point[1]; y = mathMax$1(y, rect.y); y = mathMin$1(y, rect.y + rect.height); return [x, y]; }); } /** * @param {Object} targetRect {x, y, width, height} * @param {Object} rect {x, y, width, height} * @return {Object} A new clipped rect. If rect size are negative, return undefined. */ function clipRectByRect(targetRect, rect) { var x = mathMax$1(targetRect.x, rect.x); var x2 = mathMin$1(targetRect.x + targetRect.width, rect.x + rect.width); var y = mathMax$1(targetRect.y, rect.y); var y2 = mathMin$1(targetRect.y + targetRect.height, rect.y + rect.height); // If the total rect is cliped, nothing, including the border, // should be painted. So return undefined. if (x2 >= x && y2 >= y) { return { x: x, y: y, width: x2 - x, height: y2 - y }; } } /** * @param {string} iconStr Support 'image://' or 'path://' or direct svg path. * @param {Object} [opt] Properties of `module:zrender/Element`, except `style`. * @param {Object} [rect] {x, y, width, height} * @return {module:zrender/Element} Icon path or image element. */ function createIcon(iconStr, opt, rect) { opt = extend({rectHover: true}, opt); var style = opt.style = {strokeNoScale: true}; rect = rect || {x: -1, y: -1, width: 2, height: 2}; if (iconStr) { return iconStr.indexOf('image://') === 0 ? ( style.image = iconStr.slice(8), defaults(style, rect), new ZImage(opt) ) : ( makePath( iconStr.replace('path://', ''), opt, rect, 'center' ) ); } } /** * Return `true` if the given line (line `a`) and the given polygon * are intersect. * Note that we do not count colinear as intersect here because no * requirement for that. We could do that if required in future. * * @param {number} a1x * @param {number} a1y * @param {number} a2x * @param {number} a2y * @param {Array.>} points Points of the polygon. * @return {boolean} */ function linePolygonIntersect(a1x, a1y, a2x, a2y, points) { for (var i = 0, p2 = points[points.length - 1]; i < points.length; i++) { var p = points[i]; if (lineLineIntersect(a1x, a1y, a2x, a2y, p[0], p[1], p2[0], p2[1])) { return true; } p2 = p; } } /** * Return `true` if the given two lines (line `a` and line `b`) * are intersect. * Note that we do not count colinear as intersect here because no * requirement for that. We could do that if required in future. * * @param {number} a1x * @param {number} a1y * @param {number} a2x * @param {number} a2y * @param {number} b1x * @param {number} b1y * @param {number} b2x * @param {number} b2y * @return {boolean} */ function lineLineIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) { // let `vec_m` to be `vec_a2 - vec_a1` and `vec_n` to be `vec_b2 - vec_b1`. var mx = a2x - a1x; var my = a2y - a1y; var nx = b2x - b1x; var ny = b2y - b1y; // `vec_m` and `vec_n` are parallel iff // exising `k` such that `vec_m = k · vec_n`, equivalent to `vec_m X vec_n = 0`. var nmCrossProduct = crossProduct2d(nx, ny, mx, my); if (nearZero(nmCrossProduct)) { return false; } // `vec_m` and `vec_n` are intersect iff // existing `p` and `q` in [0, 1] such that `vec_a1 + p * vec_m = vec_b1 + q * vec_n`, // such that `q = ((vec_a1 - vec_b1) X vec_m) / (vec_n X vec_m)` // and `p = ((vec_a1 - vec_b1) X vec_n) / (vec_n X vec_m)`. var b1a1x = a1x - b1x; var b1a1y = a1y - b1y; var q = crossProduct2d(b1a1x, b1a1y, mx, my) / nmCrossProduct; if (q < 0 || q > 1) { return false; } var p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct; if (p < 0 || p > 1) { return false; } return true; } /** * Cross product of 2-dimension vector. */ function crossProduct2d(x1, y1, x2, y2) { return x1 * y2 - x2 * y1; } function nearZero(val) { return val <= (1e-6) && val >= -(1e-6); } // Register built-in shapes. These shapes might be overwirtten // by users, although we do not recommend that. registerShape('circle', Circle); registerShape('sector', Sector); registerShape('ring', Ring); registerShape('polygon', Polygon); registerShape('polyline', Polyline); registerShape('rect', Rect); registerShape('line', Line); registerShape('bezierCurve', BezierCurve); registerShape('arc', Arc); var graphic = (Object.freeze || Object)({ Z2_EMPHASIS_LIFT: Z2_EMPHASIS_LIFT, CACHED_LABEL_STYLE_PROPERTIES: CACHED_LABEL_STYLE_PROPERTIES, extendShape: extendShape, extendPath: extendPath, registerShape: registerShape, getShapeClass: getShapeClass, makePath: makePath, makeImage: makeImage, mergePath: mergePath, resizePath: resizePath, subPixelOptimizeLine: subPixelOptimizeLine, subPixelOptimizeRect: subPixelOptimizeRect, subPixelOptimize: subPixelOptimize, setElementHoverStyle: setElementHoverStyle, setHoverStyle: setHoverStyle, setAsHighDownDispatcher: setAsHighDownDispatcher, isHighDownDispatcher: isHighDownDispatcher, getHighlightDigit: getHighlightDigit, setLabelStyle: setLabelStyle, modifyLabelStyle: modifyLabelStyle, setTextStyle: setTextStyle, setText: setText, getFont: getFont, updateProps: updateProps, initProps: initProps, getTransform: getTransform, applyTransform: applyTransform$1, transformDirection: transformDirection, groupTransition: groupTransition, clipPointsByRect: clipPointsByRect, clipRectByRect: clipRectByRect, createIcon: createIcon, linePolygonIntersect: linePolygonIntersect, lineLineIntersect: lineLineIntersect, Group: Group, Image: ZImage, Text: Text, Circle: Circle, Sector: Sector, Ring: Ring, Polygon: Polygon, Polyline: Polyline, Rect: Rect, Line: Line, BezierCurve: BezierCurve, Arc: Arc, IncrementalDisplayable: IncrementalDisplayble, CompoundPath: CompoundPath, LinearGradient: LinearGradient, RadialGradient: RadialGradient, BoundingRect: BoundingRect }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var PATH_COLOR = ['textStyle', 'color']; var textStyleMixin = { /** * Get color property or get color from option.textStyle.color * @param {boolean} [isEmphasis] * @return {string} */ getTextColor: function (isEmphasis) { var ecModel = this.ecModel; return this.getShallow('color') || ( (!isEmphasis && ecModel) ? ecModel.get(PATH_COLOR) : null ); }, /** * Create font string from fontStyle, fontWeight, fontSize, fontFamily * @return {string} */ getFont: function () { return getFont({ fontStyle: this.getShallow('fontStyle'), fontWeight: this.getShallow('fontWeight'), fontSize: this.getShallow('fontSize'), fontFamily: this.getShallow('fontFamily') }, this.ecModel); }, getTextRect: function (text) { return getBoundingRect( text, this.getFont(), this.getShallow('align'), this.getShallow('verticalAlign') || this.getShallow('baseline'), this.getShallow('padding'), this.getShallow('lineHeight'), this.getShallow('rich'), this.getShallow('truncateText') ); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var getItemStyle = makeStyleMapper( [ ['fill', 'color'], ['stroke', 'borderColor'], ['lineWidth', 'borderWidth'], ['opacity'], ['shadowBlur'], ['shadowOffsetX'], ['shadowOffsetY'], ['shadowColor'], ['textPosition'], ['textAlign'] ] ); var itemStyleMixin = { getItemStyle: function (excludes, includes) { var style = getItemStyle(this, excludes, includes); var lineDash = this.getBorderLineDash(); lineDash && (style.lineDash = lineDash); return style; }, getBorderLineDash: function () { var lineType = this.get('borderType'); return (lineType === 'solid' || lineType == null) ? null : (lineType === 'dashed' ? [5, 5] : [1, 1]); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/model/Model */ var mixin$1 = mixin; var inner = makeInner(); /** * @alias module:echarts/model/Model * @constructor * @param {Object} [option] * @param {module:echarts/model/Model} [parentModel] * @param {module:echarts/model/Global} [ecModel] */ function Model(option, parentModel, ecModel) { /** * @type {module:echarts/model/Model} * @readOnly */ this.parentModel = parentModel; /** * @type {module:echarts/model/Global} * @readOnly */ this.ecModel = ecModel; /** * @type {Object} * @protected */ this.option = option; // Simple optimization // if (this.init) { // if (arguments.length <= 4) { // this.init(option, parentModel, ecModel, extraOpt); // } // else { // this.init.apply(this, arguments); // } // } } Model.prototype = { constructor: Model, /** * Model 的初始化函数 * @param {Object} option */ init: null, /** * 从新的 Option merge */ mergeOption: function (option) { merge(this.option, option, true); }, /** * @param {string|Array.} path * @param {boolean} [ignoreParent=false] * @return {*} */ get: function (path, ignoreParent) { if (path == null) { return this.option; } return doGet( this.option, this.parsePath(path), !ignoreParent && getParent(this, path) ); }, /** * @param {string} key * @param {boolean} [ignoreParent=false] * @return {*} */ getShallow: function (key, ignoreParent) { var option = this.option; var val = option == null ? option : option[key]; var parentModel = !ignoreParent && getParent(this, key); if (val == null && parentModel) { val = parentModel.getShallow(key); } return val; }, /** * @param {string|Array.} [path] * @param {module:echarts/model/Model} [parentModel] * @return {module:echarts/model/Model} */ getModel: function (path, parentModel) { var obj = path == null ? this.option : doGet(this.option, path = this.parsePath(path)); var thisParentModel; parentModel = parentModel || ( (thisParentModel = getParent(this, path)) && thisParentModel.getModel(path) ); return new Model(obj, parentModel, this.ecModel); }, /** * If model has option */ isEmpty: function () { return this.option == null; }, restoreData: function () {}, // Pending clone: function () { var Ctor = this.constructor; return new Ctor(clone(this.option)); }, setReadOnly: function (properties) { // clazzUtil.setReadOnly(this, properties); }, // If path is null/undefined, return null/undefined. parsePath: function (path) { if (typeof path === 'string') { path = path.split('.'); } return path; }, /** * @param {Function} getParentMethod * param {Array.|string} path * return {module:echarts/model/Model} */ customizeGetParent: function (getParentMethod) { inner(this).getParent = getParentMethod; }, isAnimationEnabled: function () { if (!env$1.node) { if (this.option.animation != null) { return !!this.option.animation; } else if (this.parentModel) { return this.parentModel.isAnimationEnabled(); } } } }; function doGet(obj, pathArr, parentModel) { for (var i = 0; i < pathArr.length; i++) { // Ignore empty if (!pathArr[i]) { continue; } // obj could be number/string/... (like 0) obj = (obj && typeof obj === 'object') ? obj[pathArr[i]] : null; if (obj == null) { break; } } if (obj == null && parentModel) { obj = parentModel.get(pathArr); } return obj; } // `path` can be null/undefined function getParent(model, path) { var getParentMethod = inner(model).getParent; return getParentMethod ? getParentMethod.call(model, path) : model.parentModel; } // Enable Model.extend. enableClassExtend(Model); enableClassCheck(Model); mixin$1(Model, lineStyleMixin); mixin$1(Model, areaStyleMixin); mixin$1(Model, textStyleMixin); mixin$1(Model, itemStyleMixin); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var base = 0; /** * @public * @param {string} type * @return {string} */ function getUID(type) { // Considering the case of crossing js context, // use Math.random to make id as unique as possible. return [(type || ''), base++, Math.random().toFixed(5)].join('_'); } /** * @inner */ function enableSubTypeDefaulter(entity) { var subTypeDefaulters = {}; entity.registerSubTypeDefaulter = function (componentType, defaulter) { componentType = parseClassType$1(componentType); subTypeDefaulters[componentType.main] = defaulter; }; entity.determineSubType = function (componentType, option) { var type = option.type; if (!type) { var componentTypeMain = parseClassType$1(componentType).main; if (entity.hasSubTypes(componentType) && subTypeDefaulters[componentTypeMain]) { type = subTypeDefaulters[componentTypeMain](option); } } return type; }; return entity; } /** * Topological travel on Activity Network (Activity On Vertices). * Dependencies is defined in Model.prototype.dependencies, like ['xAxis', 'yAxis']. * * If 'xAxis' or 'yAxis' is absent in componentTypeList, just ignore it in topology. * * If there is circle dependencey, Error will be thrown. * */ function enableTopologicalTravel(entity, dependencyGetter) { /** * @public * @param {Array.} targetNameList Target Component type list. * Can be ['aa', 'bb', 'aa.xx'] * @param {Array.} fullNameList By which we can build dependency graph. * @param {Function} callback Params: componentType, dependencies. * @param {Object} context Scope of callback. */ entity.topologicalTravel = function (targetNameList, fullNameList, callback, context) { if (!targetNameList.length) { return; } var result = makeDepndencyGraph(fullNameList); var graph = result.graph; var stack = result.noEntryList; var targetNameSet = {}; each$1(targetNameList, function (name) { targetNameSet[name] = true; }); while (stack.length) { var currComponentType = stack.pop(); var currVertex = graph[currComponentType]; var isInTargetNameSet = !!targetNameSet[currComponentType]; if (isInTargetNameSet) { callback.call(context, currComponentType, currVertex.originalDeps.slice()); delete targetNameSet[currComponentType]; } each$1( currVertex.successor, isInTargetNameSet ? removeEdgeAndAdd : removeEdge ); } each$1(targetNameSet, function () { throw new Error('Circle dependency may exists'); }); function removeEdge(succComponentType) { graph[succComponentType].entryCount--; if (graph[succComponentType].entryCount === 0) { stack.push(succComponentType); } } // Consider this case: legend depends on series, and we call // chart.setOption({series: [...]}), where only series is in option. // If we do not have 'removeEdgeAndAdd', legendModel.mergeOption will // not be called, but only sereis.mergeOption is called. Thus legend // have no chance to update its local record about series (like which // name of series is available in legend). function removeEdgeAndAdd(succComponentType) { targetNameSet[succComponentType] = true; removeEdge(succComponentType); } }; /** * DepndencyGraph: {Object} * key: conponentType, * value: { * successor: [conponentTypes...], * originalDeps: [conponentTypes...], * entryCount: {number} * } */ function makeDepndencyGraph(fullNameList) { var graph = {}; var noEntryList = []; each$1(fullNameList, function (name) { var thisItem = createDependencyGraphItem(graph, name); var originalDeps = thisItem.originalDeps = dependencyGetter(name); var availableDeps = getAvailableDependencies(originalDeps, fullNameList); thisItem.entryCount = availableDeps.length; if (thisItem.entryCount === 0) { noEntryList.push(name); } each$1(availableDeps, function (dependentName) { if (indexOf(thisItem.predecessor, dependentName) < 0) { thisItem.predecessor.push(dependentName); } var thatItem = createDependencyGraphItem(graph, dependentName); if (indexOf(thatItem.successor, dependentName) < 0) { thatItem.successor.push(name); } }); }); return {graph: graph, noEntryList: noEntryList}; } function createDependencyGraphItem(graph, name) { if (!graph[name]) { graph[name] = {predecessor: [], successor: []}; } return graph[name]; } function getAvailableDependencies(originalDeps, fullNameList) { var availableDeps = []; each$1(originalDeps, function (dep) { indexOf(fullNameList, dep) >= 0 && availableDeps.push(dep); }); return availableDeps; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * A third-party license is embeded for some of the code in this file: * The method "quantile" was copied from "d3.js". * (See more details in the comment of the method below.) * The use of the source code of this file is also subject to the terms * and consitions of the license of "d3.js" (BSD-3Clause, see * ). */ var RADIAN_EPSILON = 1e-4; function _trim(str) { return str.replace(/^\s+|\s+$/g, ''); } /** * Linear mapping a value from domain to range * @memberOf module:echarts/util/number * @param {(number|Array.)} val * @param {Array.} domain Domain extent domain[0] can be bigger than domain[1] * @param {Array.} range Range extent range[0] can be bigger than range[1] * @param {boolean} clamp * @return {(number|Array.} */ function linearMap(val, domain, range, clamp) { var subDomain = domain[1] - domain[0]; var subRange = range[1] - range[0]; if (subDomain === 0) { return subRange === 0 ? range[0] : (range[0] + range[1]) / 2; } // Avoid accuracy problem in edge, such as // 146.39 - 62.83 === 83.55999999999999. // See echarts/test/ut/spec/util/number.js#linearMap#accuracyError // It is a little verbose for efficiency considering this method // is a hotspot. if (clamp) { if (subDomain > 0) { if (val <= domain[0]) { return range[0]; } else if (val >= domain[1]) { return range[1]; } } else { if (val >= domain[0]) { return range[0]; } else if (val <= domain[1]) { return range[1]; } } } else { if (val === domain[0]) { return range[0]; } if (val === domain[1]) { return range[1]; } } return (val - domain[0]) / subDomain * subRange + range[0]; } /** * Convert a percent string to absolute number. * Returns NaN if percent is not a valid string or number * @memberOf module:echarts/util/number * @param {string|number} percent * @param {number} all * @return {number} */ function parsePercent$1(percent, all) { switch (percent) { case 'center': case 'middle': percent = '50%'; break; case 'left': case 'top': percent = '0%'; break; case 'right': case 'bottom': percent = '100%'; break; } if (typeof percent === 'string') { if (_trim(percent).match(/%$/)) { return parseFloat(percent) / 100 * all; } return parseFloat(percent); } return percent == null ? NaN : +percent; } /** * (1) Fix rounding error of float numbers. * (2) Support return string to avoid scientific notation like '3.5e-7'. * * @param {number} x * @param {number} [precision] * @param {boolean} [returnStr] * @return {number|string} */ function round$1(x, precision, returnStr) { if (precision == null) { precision = 10; } // Avoid range error precision = Math.min(Math.max(0, precision), 20); x = (+x).toFixed(precision); return returnStr ? x : +x; } /** * asc sort arr. * The input arr will be modified. * * @param {Array} arr * @return {Array} The input arr. */ function asc(arr) { arr.sort(function (a, b) { return a - b; }); return arr; } /** * Get precision * @param {number} val */ function getPrecision(val) { val = +val; if (isNaN(val)) { return 0; } // It is much faster than methods converting number to string as follows // var tmp = val.toString(); // return tmp.length - 1 - tmp.indexOf('.'); // especially when precision is low var e = 1; var count = 0; while (Math.round(val * e) / e !== val) { e *= 10; count++; } return count; } /** * @param {string|number} val * @return {number} */ function getPrecisionSafe(val) { var str = val.toString(); // Consider scientific notation: '3.4e-12' '3.4e+12' var eIndex = str.indexOf('e'); if (eIndex > 0) { var precision = +str.slice(eIndex + 1); return precision < 0 ? -precision : 0; } else { var dotIndex = str.indexOf('.'); return dotIndex < 0 ? 0 : str.length - 1 - dotIndex; } } /** * Minimal dicernible data precisioin according to a single pixel. * * @param {Array.} dataExtent * @param {Array.} pixelExtent * @return {number} precision */ function getPixelPrecision(dataExtent, pixelExtent) { var log = Math.log; var LN10 = Math.LN10; var dataQuantity = Math.floor(log(dataExtent[1] - dataExtent[0]) / LN10); var sizeQuantity = Math.round(log(Math.abs(pixelExtent[1] - pixelExtent[0])) / LN10); // toFixed() digits argument must be between 0 and 20. var precision = Math.min(Math.max(-dataQuantity + sizeQuantity, 0), 20); return !isFinite(precision) ? 20 : precision; } /** * Get a data of given precision, assuring the sum of percentages * in valueList is 1. * The largest remainer method is used. * https://en.wikipedia.org/wiki/Largest_remainder_method * * @param {Array.} valueList a list of all data * @param {number} idx index of the data to be processed in valueList * @param {number} precision integer number showing digits of precision * @return {number} percent ranging from 0 to 100 */ function getPercentWithPrecision(valueList, idx, precision) { if (!valueList[idx]) { return 0; } var sum = reduce(valueList, function (acc, val) { return acc + (isNaN(val) ? 0 : val); }, 0); if (sum === 0) { return 0; } var digits = Math.pow(10, precision); var votesPerQuota = map(valueList, function (val) { return (isNaN(val) ? 0 : val) / sum * digits * 100; }); var targetSeats = digits * 100; var seats = map(votesPerQuota, function (votes) { // Assign automatic seats. return Math.floor(votes); }); var currentSum = reduce(seats, function (acc, val) { return acc + val; }, 0); var remainder = map(votesPerQuota, function (votes, idx) { return votes - seats[idx]; }); // Has remainding votes. while (currentSum < targetSeats) { // Find next largest remainder. var max = Number.NEGATIVE_INFINITY; var maxId = null; for (var i = 0, len = remainder.length; i < len; ++i) { if (remainder[i] > max) { max = remainder[i]; maxId = i; } } // Add a vote to max remainder. ++seats[maxId]; remainder[maxId] = 0; ++currentSum; } return seats[idx] / digits; } // Number.MAX_SAFE_INTEGER, ie do not support. var MAX_SAFE_INTEGER = 9007199254740991; /** * To 0 - 2 * PI, considering negative radian. * @param {number} radian * @return {number} */ function remRadian(radian) { var pi2 = Math.PI * 2; return (radian % pi2 + pi2) % pi2; } /** * @param {type} radian * @return {boolean} */ function isRadianAroundZero(val) { return val > -RADIAN_EPSILON && val < RADIAN_EPSILON; } /* eslint-disable */ var TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/; // jshint ignore:line /* eslint-enable */ /** * @param {string|Date|number} value These values can be accepted: * + An instance of Date, represent a time in its own time zone. * + Or string in a subset of ISO 8601, only including: * + only year, month, date: '2012-03', '2012-03-01', '2012-03-01 05', '2012-03-01 05:06', * + separated with T or space: '2012-03-01T12:22:33.123', '2012-03-01 12:22:33.123', * + time zone: '2012-03-01T12:22:33Z', '2012-03-01T12:22:33+8000', '2012-03-01T12:22:33-05:00', * all of which will be treated as local time if time zone is not specified * (see ). * + Or other string format, including (all of which will be treated as loacal time): * '2012', '2012-3-1', '2012/3/1', '2012/03/01', * '2009/6/12 2:00', '2009/6/12 2:05:08', '2009/6/12 2:05:08.123' * + a timestamp, which represent a time in UTC. * @return {Date} date */ function parseDate(value) { if (value instanceof Date) { return value; } else if (typeof value === 'string') { // Different browsers parse date in different way, so we parse it manually. // Some other issues: // new Date('1970-01-01') is UTC, // new Date('1970/01/01') and new Date('1970-1-01') is local. // See issue #3623 var match = TIME_REG.exec(value); if (!match) { // return Invalid Date. return new Date(NaN); } // Use local time when no timezone offset specifed. if (!match[8]) { // match[n] can only be string or undefined. // But take care of '12' + 1 => '121'. return new Date( +match[1], +(match[2] || 1) - 1, +match[3] || 1, +match[4] || 0, +(match[5] || 0), +match[6] || 0, +match[7] || 0 ); } // Timezoneoffset of Javascript Date has considered DST (Daylight Saving Time, // https://tc39.github.io/ecma262/#sec-daylight-saving-time-adjustment). // For example, system timezone is set as "Time Zone: America/Toronto", // then these code will get different result: // `new Date(1478411999999).getTimezoneOffset(); // get 240` // `new Date(1478412000000).getTimezoneOffset(); // get 300` // So we should not use `new Date`, but use `Date.UTC`. else { var hour = +match[4] || 0; if (match[8].toUpperCase() !== 'Z') { hour -= match[8].slice(0, 3); } return new Date(Date.UTC( +match[1], +(match[2] || 1) - 1, +match[3] || 1, hour, +(match[5] || 0), +match[6] || 0, +match[7] || 0 )); } } else if (value == null) { return new Date(NaN); } return new Date(Math.round(value)); } /** * Quantity of a number. e.g. 0.1, 1, 10, 100 * * @param {number} val * @return {number} */ function quantity(val) { return Math.pow(10, quantityExponent(val)); } /** * Exponent of the quantity of a number * e.g., 1234 equals to 1.234*10^3, so quantityExponent(1234) is 3 * * @param {number} val non-negative value * @return {number} */ function quantityExponent(val) { if (val === 0) { return 0; } var exp = Math.floor(Math.log(val) / Math.LN10); /** * exp is expected to be the rounded-down result of the base-10 log of val. * But due to the precision loss with Math.log(val), we need to restore it * using 10^exp to make sure we can get val back from exp. #11249 */ if (val / Math.pow(10, exp) >= 10) { exp++; } return exp; } /** * find a “nice” number approximately equal to x. Round the number if round = true, * take ceiling if round = false. The primary observation is that the “nicest” * numbers in decimal are 1, 2, and 5, and all power-of-ten multiples of these numbers. * * See "Nice Numbers for Graph Labels" of Graphic Gems. * * @param {number} val Non-negative value. * @param {boolean} round * @return {number} */ function nice(val, round) { var exponent = quantityExponent(val); var exp10 = Math.pow(10, exponent); var f = val / exp10; // 1 <= f < 10 var nf; if (round) { if (f < 1.5) { nf = 1; } else if (f < 2.5) { nf = 2; } else if (f < 4) { nf = 3; } else if (f < 7) { nf = 5; } else { nf = 10; } } else { if (f < 1) { nf = 1; } else if (f < 2) { nf = 2; } else if (f < 3) { nf = 3; } else if (f < 5) { nf = 5; } else { nf = 10; } } val = nf * exp10; // Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754). // 20 is the uppper bound of toFixed. return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val; } /** * This code was copied from "d3.js" * . * See the license statement at the head of this file. * @param {Array.} ascArr */ function quantile(ascArr, p) { var H = (ascArr.length - 1) * p + 1; var h = Math.floor(H); var v = +ascArr[h - 1]; var e = H - h; return e ? v + e * (ascArr[h] - v) : v; } /** * Order intervals asc, and split them when overlap. * expect(numberUtil.reformIntervals([ * {interval: [18, 62], close: [1, 1]}, * {interval: [-Infinity, -70], close: [0, 0]}, * {interval: [-70, -26], close: [1, 1]}, * {interval: [-26, 18], close: [1, 1]}, * {interval: [62, 150], close: [1, 1]}, * {interval: [106, 150], close: [1, 1]}, * {interval: [150, Infinity], close: [0, 0]} * ])).toEqual([ * {interval: [-Infinity, -70], close: [0, 0]}, * {interval: [-70, -26], close: [1, 1]}, * {interval: [-26, 18], close: [0, 1]}, * {interval: [18, 62], close: [0, 1]}, * {interval: [62, 150], close: [0, 1]}, * {interval: [150, Infinity], close: [0, 0]} * ]); * @param {Array.} list, where `close` mean open or close * of the interval, and Infinity can be used. * @return {Array.} The origin list, which has been reformed. */ function reformIntervals(list) { list.sort(function (a, b) { return littleThan(a, b, 0) ? -1 : 1; }); var curr = -Infinity; var currClose = 1; for (var i = 0; i < list.length;) { var interval = list[i].interval; var close = list[i].close; for (var lg = 0; lg < 2; lg++) { if (interval[lg] <= curr) { interval[lg] = curr; close[lg] = !lg ? 1 - currClose : 1; } curr = interval[lg]; currClose = close[lg]; } if (interval[0] === interval[1] && close[0] * close[1] !== 1) { list.splice(i, 1); } else { i++; } } return list; function littleThan(a, b, lg) { return a.interval[lg] < b.interval[lg] || ( a.interval[lg] === b.interval[lg] && ( (a.close[lg] - b.close[lg] === (!lg ? 1 : -1)) || (!lg && littleThan(a, b, 1)) ) ); } } /** * parseFloat NaNs numeric-cast false positives (null|true|false|"") * ...but misinterprets leading-number strings, particularly hex literals ("0x...") * subtraction forces infinities to NaN * * @param {*} v * @return {boolean} */ function isNumeric(v) { return v - parseFloat(v) >= 0; } var number = (Object.freeze || Object)({ linearMap: linearMap, parsePercent: parsePercent$1, round: round$1, asc: asc, getPrecision: getPrecision, getPrecisionSafe: getPrecisionSafe, getPixelPrecision: getPixelPrecision, getPercentWithPrecision: getPercentWithPrecision, MAX_SAFE_INTEGER: MAX_SAFE_INTEGER, remRadian: remRadian, isRadianAroundZero: isRadianAroundZero, parseDate: parseDate, quantity: quantity, quantityExponent: quantityExponent, nice: nice, quantile: quantile, reformIntervals: reformIntervals, isNumeric: isNumeric }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import Text from 'zrender/src/graphic/Text'; /** * add commas after every three numbers * @param {string|number} x * @return {string} */ function addCommas(x) { if (isNaN(x)) { return '-'; } x = (x + '').split('.'); return x[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g, '$1,') + (x.length > 1 ? ('.' + x[1]) : ''); } /** * @param {string} str * @param {boolean} [upperCaseFirst=false] * @return {string} str */ function toCamelCase(str, upperCaseFirst) { str = (str || '').toLowerCase().replace(/-(.)/g, function (match, group1) { return group1.toUpperCase(); }); if (upperCaseFirst && str) { str = str.charAt(0).toUpperCase() + str.slice(1); } return str; } var normalizeCssArray$1 = normalizeCssArray; var replaceReg = /([&<>"'])/g; var replaceMap = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }; function encodeHTML(source) { return source == null ? '' : (source + '').replace(replaceReg, function (str, c) { return replaceMap[c]; }); } var TPL_VAR_ALIAS = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; var wrapVar = function (varName, seriesIdx) { return '{' + varName + (seriesIdx == null ? '' : seriesIdx) + '}'; }; /** * Template formatter * @param {string} tpl * @param {Array.|Object} paramsList * @param {boolean} [encode=false] * @return {string} */ function formatTpl(tpl, paramsList, encode) { if (!isArray(paramsList)) { paramsList = [paramsList]; } var seriesLen = paramsList.length; if (!seriesLen) { return ''; } var $vars = paramsList[0].$vars || []; for (var i = 0; i < $vars.length; i++) { var alias = TPL_VAR_ALIAS[i]; tpl = tpl.replace(wrapVar(alias), wrapVar(alias, 0)); } for (var seriesIdx = 0; seriesIdx < seriesLen; seriesIdx++) { for (var k = 0; k < $vars.length; k++) { var val = paramsList[seriesIdx][$vars[k]]; tpl = tpl.replace( wrapVar(TPL_VAR_ALIAS[k], seriesIdx), encode ? encodeHTML(val) : val ); } } return tpl; } /** * simple Template formatter * * @param {string} tpl * @param {Object} param * @param {boolean} [encode=false] * @return {string} */ function formatTplSimple(tpl, param, encode) { each$1(param, function (value, key) { tpl = tpl.replace( '{' + key + '}', encode ? encodeHTML(value) : value ); }); return tpl; } /** * @param {Object|string} [opt] If string, means color. * @param {string} [opt.color] * @param {string} [opt.extraCssText] * @param {string} [opt.type='item'] 'item' or 'subItem' * @param {string} [opt.renderMode='html'] render mode of tooltip, 'html' or 'richText' * @param {string} [opt.markerId='X'] id name for marker. If only one marker is in a rich text, this can be omitted. * @return {string} */ function getTooltipMarker(opt, extraCssText) { opt = isString(opt) ? {color: opt, extraCssText: extraCssText} : (opt || {}); var color = opt.color; var type = opt.type; var extraCssText = opt.extraCssText; var renderMode = opt.renderMode || 'html'; var markerId = opt.markerId || 'X'; if (!color) { return ''; } if (renderMode === 'html') { return type === 'subItem' ? '' : ''; } else { // Space for rich element marker return { renderMode: renderMode, content: '{marker' + markerId + '|} ', style: { color: color } }; } } function pad(str, len) { str += ''; return '0000'.substr(0, len - str.length) + str; } /** * ISO Date format * @param {string} tpl * @param {number} value * @param {boolean} [isUTC=false] Default in local time. * see `module:echarts/scale/Time` * and `module:echarts/util/number#parseDate`. * @inner */ function formatTime(tpl, value, isUTC) { if (tpl === 'week' || tpl === 'month' || tpl === 'quarter' || tpl === 'half-year' || tpl === 'year' ) { tpl = 'MM-dd\nyyyy'; } var date = parseDate(value); var utc = isUTC ? 'UTC' : ''; var y = date['get' + utc + 'FullYear'](); var M = date['get' + utc + 'Month']() + 1; var d = date['get' + utc + 'Date'](); var h = date['get' + utc + 'Hours'](); var m = date['get' + utc + 'Minutes'](); var s = date['get' + utc + 'Seconds'](); var S = date['get' + utc + 'Milliseconds'](); tpl = tpl.replace('MM', pad(M, 2)) .replace('M', M) .replace('yyyy', y) .replace('yy', y % 100) .replace('dd', pad(d, 2)) .replace('d', d) .replace('hh', pad(h, 2)) .replace('h', h) .replace('mm', pad(m, 2)) .replace('m', m) .replace('ss', pad(s, 2)) .replace('s', s) .replace('SSS', pad(S, 3)); return tpl; } /** * Capital first * @param {string} str * @return {string} */ function capitalFirst(str) { return str ? str.charAt(0).toUpperCase() + str.substr(1) : str; } var truncateText$1 = truncateText; /** * @public * @param {Object} opt * @param {string} opt.text * @param {string} opt.font * @param {string} [opt.textAlign='left'] * @param {string} [opt.textVerticalAlign='top'] * @param {Array.} [opt.textPadding] * @param {number} [opt.textLineHeight] * @param {Object} [opt.rich] * @param {Object} [opt.truncate] * @return {Object} {x, y, width, height, lineHeight} */ function getTextBoundingRect(opt) { return getBoundingRect( opt.text, opt.font, opt.textAlign, opt.textVerticalAlign, opt.textPadding, opt.textLineHeight, opt.rich, opt.truncate ); } /** * @deprecated * the `textLineHeight` was added later. * For backward compatiblility, put it as the last parameter. * But deprecated this interface. Please use `getTextBoundingRect` instead. */ function getTextRect( text, font, textAlign, textVerticalAlign, textPadding, rich, truncate, textLineHeight ) { return getBoundingRect( text, font, textAlign, textVerticalAlign, textPadding, textLineHeight, rich, truncate ); } /** * open new tab * @param {string} link url * @param {string} target blank or self */ function windowOpen(link, target) { if (target === '_blank' || target === 'blank') { var blank = window.open(); blank.opener = null; blank.location = link; } else { window.open(link, target); } } var format = (Object.freeze || Object)({ addCommas: addCommas, toCamelCase: toCamelCase, normalizeCssArray: normalizeCssArray$1, encodeHTML: encodeHTML, formatTpl: formatTpl, formatTplSimple: formatTplSimple, getTooltipMarker: getTooltipMarker, formatTime: formatTime, capitalFirst: capitalFirst, truncateText: truncateText$1, getTextBoundingRect: getTextBoundingRect, getTextRect: getTextRect, windowOpen: windowOpen }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Layout helpers for each component positioning var each$3 = each$1; /** * @public */ var LOCATION_PARAMS = [ 'left', 'right', 'top', 'bottom', 'width', 'height' ]; /** * @public */ var HV_NAMES = [ ['width', 'left', 'right'], ['height', 'top', 'bottom'] ]; function boxLayout(orient, group, gap, maxWidth, maxHeight) { var x = 0; var y = 0; if (maxWidth == null) { maxWidth = Infinity; } if (maxHeight == null) { maxHeight = Infinity; } var currentLineMaxSize = 0; group.eachChild(function (child, idx) { var position = child.position; var rect = child.getBoundingRect(); var nextChild = group.childAt(idx + 1); var nextChildRect = nextChild && nextChild.getBoundingRect(); var nextX; var nextY; if (orient === 'horizontal') { var moveX = rect.width + (nextChildRect ? (-nextChildRect.x + rect.x) : 0); nextX = x + moveX; // Wrap when width exceeds maxWidth or meet a `newline` group // FIXME compare before adding gap? if (nextX > maxWidth || child.newline) { x = 0; nextX = moveX; y += currentLineMaxSize + gap; currentLineMaxSize = rect.height; } else { // FIXME: consider rect.y is not `0`? currentLineMaxSize = Math.max(currentLineMaxSize, rect.height); } } else { var moveY = rect.height + (nextChildRect ? (-nextChildRect.y + rect.y) : 0); nextY = y + moveY; // Wrap when width exceeds maxHeight or meet a `newline` group if (nextY > maxHeight || child.newline) { x += currentLineMaxSize + gap; y = 0; nextY = moveY; currentLineMaxSize = rect.width; } else { currentLineMaxSize = Math.max(currentLineMaxSize, rect.width); } } if (child.newline) { return; } position[0] = x; position[1] = y; orient === 'horizontal' ? (x = nextX + gap) : (y = nextY + gap); }); } /** * VBox or HBox layouting * @param {string} orient * @param {module:zrender/container/Group} group * @param {number} gap * @param {number} [width=Infinity] * @param {number} [height=Infinity] */ var box = boxLayout; /** * VBox layouting * @param {module:zrender/container/Group} group * @param {number} gap * @param {number} [width=Infinity] * @param {number} [height=Infinity] */ var vbox = curry(boxLayout, 'vertical'); /** * HBox layouting * @param {module:zrender/container/Group} group * @param {number} gap * @param {number} [width=Infinity] * @param {number} [height=Infinity] */ var hbox = curry(boxLayout, 'horizontal'); /** * If x or x2 is not specified or 'center' 'left' 'right', * the width would be as long as possible. * If y or y2 is not specified or 'middle' 'top' 'bottom', * the height would be as long as possible. * * @param {Object} positionInfo * @param {number|string} [positionInfo.x] * @param {number|string} [positionInfo.y] * @param {number|string} [positionInfo.x2] * @param {number|string} [positionInfo.y2] * @param {Object} containerRect {width, height} * @param {string|number} margin * @return {Object} {width, height} */ /** * Parse position info. * * @param {Object} positionInfo * @param {number|string} [positionInfo.left] * @param {number|string} [positionInfo.top] * @param {number|string} [positionInfo.right] * @param {number|string} [positionInfo.bottom] * @param {number|string} [positionInfo.width] * @param {number|string} [positionInfo.height] * @param {number|string} [positionInfo.aspect] Aspect is width / height * @param {Object} containerRect * @param {string|number} [margin] * * @return {module:zrender/core/BoundingRect} */ function getLayoutRect( positionInfo, containerRect, margin ) { margin = normalizeCssArray$1(margin || 0); var containerWidth = containerRect.width; var containerHeight = containerRect.height; var left = parsePercent$1(positionInfo.left, containerWidth); var top = parsePercent$1(positionInfo.top, containerHeight); var right = parsePercent$1(positionInfo.right, containerWidth); var bottom = parsePercent$1(positionInfo.bottom, containerHeight); var width = parsePercent$1(positionInfo.width, containerWidth); var height = parsePercent$1(positionInfo.height, containerHeight); var verticalMargin = margin[2] + margin[0]; var horizontalMargin = margin[1] + margin[3]; var aspect = positionInfo.aspect; // If width is not specified, calculate width from left and right if (isNaN(width)) { width = containerWidth - right - horizontalMargin - left; } if (isNaN(height)) { height = containerHeight - bottom - verticalMargin - top; } if (aspect != null) { // If width and height are not given // 1. Graph should not exceeds the container // 2. Aspect must be keeped // 3. Graph should take the space as more as possible // FIXME // Margin is not considered, because there is no case that both // using margin and aspect so far. if (isNaN(width) && isNaN(height)) { if (aspect > containerWidth / containerHeight) { width = containerWidth * 0.8; } else { height = containerHeight * 0.8; } } // Calculate width or height with given aspect if (isNaN(width)) { width = aspect * height; } if (isNaN(height)) { height = width / aspect; } } // If left is not specified, calculate left from right and width if (isNaN(left)) { left = containerWidth - right - width - horizontalMargin; } if (isNaN(top)) { top = containerHeight - bottom - height - verticalMargin; } // Align left and top switch (positionInfo.left || positionInfo.right) { case 'center': left = containerWidth / 2 - width / 2 - margin[3]; break; case 'right': left = containerWidth - width - horizontalMargin; break; } switch (positionInfo.top || positionInfo.bottom) { case 'middle': case 'center': top = containerHeight / 2 - height / 2 - margin[0]; break; case 'bottom': top = containerHeight - height - verticalMargin; break; } // If something is wrong and left, top, width, height are calculated as NaN left = left || 0; top = top || 0; if (isNaN(width)) { // Width may be NaN if only one value is given except width width = containerWidth - horizontalMargin - left - (right || 0); } if (isNaN(height)) { // Height may be NaN if only one value is given except height height = containerHeight - verticalMargin - top - (bottom || 0); } var rect = new BoundingRect(left + margin[3], top + margin[0], width, height); rect.margin = margin; return rect; } /** * Position a zr element in viewport * Group position is specified by either * {left, top}, {right, bottom} * If all properties exists, right and bottom will be igonred. * * Logic: * 1. Scale (against origin point in parent coord) * 2. Rotate (against origin point in parent coord) * 3. Traslate (with el.position by this method) * So this method only fixes the last step 'Traslate', which does not affect * scaling and rotating. * * If be called repeatly with the same input el, the same result will be gotten. * * @param {module:zrender/Element} el Should have `getBoundingRect` method. * @param {Object} positionInfo * @param {number|string} [positionInfo.left] * @param {number|string} [positionInfo.top] * @param {number|string} [positionInfo.right] * @param {number|string} [positionInfo.bottom] * @param {number|string} [positionInfo.width] Only for opt.boundingModel: 'raw' * @param {number|string} [positionInfo.height] Only for opt.boundingModel: 'raw' * @param {Object} containerRect * @param {string|number} margin * @param {Object} [opt] * @param {Array.} [opt.hv=[1,1]] Only horizontal or only vertical. * @param {Array.} [opt.boundingMode='all'] * Specify how to calculate boundingRect when locating. * 'all': Position the boundingRect that is transformed and uioned * both itself and its descendants. * This mode simplies confine the elements in the bounding * of their container (e.g., using 'right: 0'). * 'raw': Position the boundingRect that is not transformed and only itself. * This mode is useful when you want a element can overflow its * container. (Consider a rotated circle needs to be located in a corner.) * In this mode positionInfo.width/height can only be number. */ function positionElement(el, positionInfo, containerRect, margin, opt) { var h = !opt || !opt.hv || opt.hv[0]; var v = !opt || !opt.hv || opt.hv[1]; var boundingMode = opt && opt.boundingMode || 'all'; if (!h && !v) { return; } var rect; if (boundingMode === 'raw') { rect = el.type === 'group' ? new BoundingRect(0, 0, +positionInfo.width || 0, +positionInfo.height || 0) : el.getBoundingRect(); } else { rect = el.getBoundingRect(); if (el.needLocalTransform()) { var transform = el.getLocalTransform(); // Notice: raw rect may be inner object of el, // which should not be modified. rect = rect.clone(); rect.applyTransform(transform); } } // The real width and height can not be specified but calculated by the given el. positionInfo = getLayoutRect( defaults( {width: rect.width, height: rect.height}, positionInfo ), containerRect, margin ); // Because 'tranlate' is the last step in transform // (see zrender/core/Transformable#getLocalTransform), // we can just only modify el.position to get final result. var elPos = el.position; var dx = h ? positionInfo.x - rect.x : 0; var dy = v ? positionInfo.y - rect.y : 0; el.attr('position', boundingMode === 'raw' ? [dx, dy] : [elPos[0] + dx, elPos[1] + dy]); } /** * @param {Object} option Contains some of the properties in HV_NAMES. * @param {number} hvIdx 0: horizontal; 1: vertical. */ /** * Consider Case: * When defulat option has {left: 0, width: 100}, and we set {right: 0} * through setOption or media query, using normal zrUtil.merge will cause * {right: 0} does not take effect. * * @example * ComponentModel.extend({ * init: function () { * ... * var inputPositionParams = layout.getLayoutParams(option); * this.mergeOption(inputPositionParams); * }, * mergeOption: function (newOption) { * newOption && zrUtil.merge(thisOption, newOption, true); * layout.mergeLayoutParam(thisOption, newOption); * } * }); * * @param {Object} targetOption * @param {Object} newOption * @param {Object|string} [opt] * @param {boolean|Array.} [opt.ignoreSize=false] Used for the components * that width (or height) should not be calculated by left and right (or top and bottom). */ function mergeLayoutParam(targetOption, newOption, opt) { !isObject$1(opt) && (opt = {}); var ignoreSize = opt.ignoreSize; !isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]); var hResult = merge$$1(HV_NAMES[0], 0); var vResult = merge$$1(HV_NAMES[1], 1); copy(HV_NAMES[0], targetOption, hResult); copy(HV_NAMES[1], targetOption, vResult); function merge$$1(names, hvIdx) { var newParams = {}; var newValueCount = 0; var merged = {}; var mergedValueCount = 0; var enoughParamNumber = 2; each$3(names, function (name) { merged[name] = targetOption[name]; }); each$3(names, function (name) { // Consider case: newOption.width is null, which is // set by user for removing width setting. hasProp(newOption, name) && (newParams[name] = merged[name] = newOption[name]); hasValue(newParams, name) && newValueCount++; hasValue(merged, name) && mergedValueCount++; }); if (ignoreSize[hvIdx]) { // Only one of left/right is premitted to exist. if (hasValue(newOption, names[1])) { merged[names[2]] = null; } else if (hasValue(newOption, names[2])) { merged[names[1]] = null; } return merged; } // Case: newOption: {width: ..., right: ...}, // or targetOption: {right: ...} and newOption: {width: ...}, // There is no conflict when merged only has params count // little than enoughParamNumber. if (mergedValueCount === enoughParamNumber || !newValueCount) { return merged; } // Case: newOption: {width: ..., right: ...}, // Than we can make sure user only want those two, and ignore // all origin params in targetOption. else if (newValueCount >= enoughParamNumber) { return newParams; } else { // Chose another param from targetOption by priority. for (var i = 0; i < names.length; i++) { var name = names[i]; if (!hasProp(newParams, name) && hasProp(targetOption, name)) { newParams[name] = targetOption[name]; break; } } return newParams; } } function hasProp(obj, name) { return obj.hasOwnProperty(name); } function hasValue(obj, name) { return obj[name] != null && obj[name] !== 'auto'; } function copy(names, target, source) { each$3(names, function (name) { target[name] = source[name]; }); } } /** * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object. * @param {Object} source * @return {Object} Result contains those props. */ function getLayoutParams(source) { return copyLayoutParams({}, source); } /** * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object. * @param {Object} source * @return {Object} Result contains those props. */ function copyLayoutParams(target, source) { source && target && each$3(LOCATION_PARAMS, function (name) { source.hasOwnProperty(name) && (target[name] = source[name]); }); return target; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var boxLayoutMixin = { getBoxLayoutParams: function () { return { left: this.get('left'), top: this.get('top'), right: this.get('right'), bottom: this.get('bottom'), width: this.get('width'), height: this.get('height') }; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Component model * * @module echarts/model/Component */ var inner$1 = makeInner(); /** * @alias module:echarts/model/Component * @constructor * @param {Object} option * @param {module:echarts/model/Model} parentModel * @param {module:echarts/model/Model} ecModel */ var ComponentModel = Model.extend({ type: 'component', /** * @readOnly * @type {string} */ id: '', /** * Because simplified concept is probably better, series.name (or component.name) * has been having too many resposibilities: * (1) Generating id (which requires name in option should not be modified). * (2) As an index to mapping series when merging option or calling API (a name * can refer to more then one components, which is convinient is some case). * (3) Display. * @readOnly */ name: '', /** * @readOnly * @type {string} */ mainType: '', /** * @readOnly * @type {string} */ subType: '', /** * @readOnly * @type {number} */ componentIndex: 0, /** * @type {Object} * @protected */ defaultOption: null, /** * @type {module:echarts/model/Global} * @readOnly */ ecModel: null, /** * key: componentType * value: Component model list, can not be null. * @type {Object.>} * @readOnly */ dependentModels: [], /** * @type {string} * @readOnly */ uid: null, /** * Support merge layout params. * Only support 'box' now (left/right/top/bottom/width/height). * @type {string|Object} Object can be {ignoreSize: true} * @readOnly */ layoutMode: null, $constructor: function (option, parentModel, ecModel, extraOpt) { Model.call(this, option, parentModel, ecModel, extraOpt); this.uid = getUID('ec_cpt_model'); }, init: function (option, parentModel, ecModel, extraOpt) { this.mergeDefaultAndTheme(option, ecModel); }, mergeDefaultAndTheme: function (option, ecModel) { var layoutMode = this.layoutMode; var inputPositionParams = layoutMode ? getLayoutParams(option) : {}; var themeModel = ecModel.getTheme(); merge(option, themeModel.get(this.mainType)); merge(option, this.getDefaultOption()); if (layoutMode) { mergeLayoutParam(option, inputPositionParams, layoutMode); } }, mergeOption: function (option, extraOpt) { merge(this.option, option, true); var layoutMode = this.layoutMode; if (layoutMode) { mergeLayoutParam(this.option, option, layoutMode); } }, // Hooker after init or mergeOption optionUpdated: function (newCptOption, isInit) {}, getDefaultOption: function () { var fields = inner$1(this); if (!fields.defaultOption) { var optList = []; var Class = this.constructor; while (Class) { var opt = Class.prototype.defaultOption; opt && optList.push(opt); Class = Class.superClass; } var defaultOption = {}; for (var i = optList.length - 1; i >= 0; i--) { defaultOption = merge(defaultOption, optList[i], true); } fields.defaultOption = defaultOption; } return fields.defaultOption; }, getReferringComponents: function (mainType) { return this.ecModel.queryComponents({ mainType: mainType, index: this.get(mainType + 'Index', true), id: this.get(mainType + 'Id', true) }); } }); // Reset ComponentModel.extend, add preConstruct. // clazzUtil.enableClassExtend( // ComponentModel, // function (option, parentModel, ecModel, extraOpt) { // // Set dependentModels, componentIndex, name, id, mainType, subType. // zrUtil.extend(this, extraOpt); // this.uid = componentUtil.getUID('componentModel'); // // this.setReadOnly([ // // 'type', 'id', 'uid', 'name', 'mainType', 'subType', // // 'dependentModels', 'componentIndex' // // ]); // } // ); // Add capability of registerClass, getClass, hasClass, registerSubTypeDefaulter and so on. enableClassManagement( ComponentModel, {registerWhenExtend: true} ); enableSubTypeDefaulter(ComponentModel); // Add capability of ComponentModel.topologicalTravel. enableTopologicalTravel(ComponentModel, getDependencies); function getDependencies(componentType) { var deps = []; each$1(ComponentModel.getClassesByMainType(componentType), function (Clazz) { deps = deps.concat(Clazz.prototype.dependencies || []); }); // Ensure main type. deps = map(deps, function (type) { return parseClassType$1(type).main; }); // Hack dataset for convenience. if (componentType !== 'dataset' && indexOf(deps, 'dataset') <= 0) { deps.unshift('dataset'); } return deps; } mixin(ComponentModel, boxLayoutMixin); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var platform = ''; // Navigator not exists in node if (typeof navigator !== 'undefined') { platform = navigator.platform || ''; } var globalDefault = { // backgroundColor: 'rgba(0,0,0,0)', // https://dribbble.com/shots/1065960-Infographic-Pie-chart-visualization // color: ['#5793f3', '#d14a61', '#fd9c35', '#675bba', '#fec42c', '#dd4444', '#d4df5a', '#cd4870'], // Light colors: // color: ['#bcd3bb', '#e88f70', '#edc1a5', '#9dc5c8', '#e1e8c8', '#7b7c68', '#e5b5b5', '#f0b489', '#928ea8', '#bda29a'], // color: ['#cc5664', '#9bd6ec', '#ea946e', '#8acaaa', '#f1ec64', '#ee8686', '#a48dc1', '#5da6bc', '#b9dcae'], // Dark colors: color: [ '#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3' ], gradientColor: ['#f6efa6', '#d88273', '#bf444c'], // If xAxis and yAxis declared, grid is created by default. // grid: {}, textStyle: { // color: '#000', // decoration: 'none', // PENDING fontFamily: platform.match(/^Win/) ? 'Microsoft YaHei' : 'sans-serif', // fontFamily: 'Arial, Verdana, sans-serif', fontSize: 12, fontStyle: 'normal', fontWeight: 'normal' }, // http://blogs.adobe.com/webplatform/2014/02/24/using-blend-modes-in-html-canvas/ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation // Default is source-over blendMode: null, animation: 'auto', animationDuration: 1000, animationDurationUpdate: 300, animationEasing: 'exponentialOut', animationEasingUpdate: 'cubicOut', animationThreshold: 2000, // Configuration for progressive/incremental rendering progressiveThreshold: 3000, progressive: 400, // Threshold of if use single hover layer to optimize. // It is recommended that `hoverLayerThreshold` is equivalent to or less than // `progressiveThreshold`, otherwise hover will cause restart of progressive, // which is unexpected. // see example . hoverLayerThreshold: 3000, // See: module:echarts/scale/Time useUTC: false }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$2 = makeInner(); function getNearestColorPalette(colors, requestColorNum) { var paletteNum = colors.length; // TODO colors must be in order for (var i = 0; i < paletteNum; i++) { if (colors[i].length > requestColorNum) { return colors[i]; } } return colors[paletteNum - 1]; } var colorPaletteMixin = { clearColorPalette: function () { inner$2(this).colorIdx = 0; inner$2(this).colorNameMap = {}; }, /** * @param {string} name MUST NOT be null/undefined. Otherwise call this function * twise with the same parameters will get different result. * @param {Object} [scope=this] * @param {Object} [requestColorNum] * @return {string} color string. */ getColorFromPalette: function (name, scope, requestColorNum) { scope = scope || this; var scopeFields = inner$2(scope); var colorIdx = scopeFields.colorIdx || 0; var colorNameMap = scopeFields.colorNameMap = scopeFields.colorNameMap || {}; // Use `hasOwnProperty` to avoid conflict with Object.prototype. if (colorNameMap.hasOwnProperty(name)) { return colorNameMap[name]; } var defaultColorPalette = normalizeToArray(this.get('color', true)); var layeredColorPalette = this.get('colorLayer', true); var colorPalette = ((requestColorNum == null || !layeredColorPalette) ? defaultColorPalette : getNearestColorPalette(layeredColorPalette, requestColorNum)); // In case can't find in layered color palette. colorPalette = colorPalette || defaultColorPalette; if (!colorPalette || !colorPalette.length) { return; } var color = colorPalette[colorIdx]; if (name) { colorNameMap[name] = color; } scopeFields.colorIdx = (colorIdx + 1) % colorPalette.length; return color; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Avoid typo. var SOURCE_FORMAT_ORIGINAL = 'original'; var SOURCE_FORMAT_ARRAY_ROWS = 'arrayRows'; var SOURCE_FORMAT_OBJECT_ROWS = 'objectRows'; var SOURCE_FORMAT_KEYED_COLUMNS = 'keyedColumns'; var SOURCE_FORMAT_UNKNOWN = 'unknown'; // ??? CHANGE A NAME var SOURCE_FORMAT_TYPED_ARRAY = 'typedArray'; var SERIES_LAYOUT_BY_COLUMN = 'column'; var SERIES_LAYOUT_BY_ROW = 'row'; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * [sourceFormat] * * + "original": * This format is only used in series.data, where * itemStyle can be specified in data item. * * + "arrayRows": * [ * ['product', 'score', 'amount'], * ['Matcha Latte', 89.3, 95.8], * ['Milk Tea', 92.1, 89.4], * ['Cheese Cocoa', 94.4, 91.2], * ['Walnut Brownie', 85.4, 76.9] * ] * * + "objectRows": * [ * {product: 'Matcha Latte', score: 89.3, amount: 95.8}, * {product: 'Milk Tea', score: 92.1, amount: 89.4}, * {product: 'Cheese Cocoa', score: 94.4, amount: 91.2}, * {product: 'Walnut Brownie', score: 85.4, amount: 76.9} * ] * * + "keyedColumns": * { * 'product': ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie'], * 'count': [823, 235, 1042, 988], * 'score': [95.8, 81.4, 91.2, 76.9] * } * * + "typedArray" * * + "unknown" */ /** * @constructor * @param {Object} fields * @param {string} fields.sourceFormat * @param {Array|Object} fields.fromDataset * @param {Array|Object} [fields.data] * @param {string} [seriesLayoutBy='column'] * @param {Array.} [dimensionsDefine] * @param {Objet|HashMap} [encodeDefine] * @param {number} [startIndex=0] * @param {number} [dimensionsDetectCount] */ function Source(fields) { /** * @type {boolean} */ this.fromDataset = fields.fromDataset; /** * Not null/undefined. * @type {Array|Object} */ this.data = fields.data || ( fields.sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS ? {} : [] ); /** * See also "detectSourceFormat". * Not null/undefined. * @type {string} */ this.sourceFormat = fields.sourceFormat || SOURCE_FORMAT_UNKNOWN; /** * 'row' or 'column' * Not null/undefined. * @type {string} seriesLayoutBy */ this.seriesLayoutBy = fields.seriesLayoutBy || SERIES_LAYOUT_BY_COLUMN; /** * dimensions definition in option. * can be null/undefined. * @type {Array.} */ this.dimensionsDefine = fields.dimensionsDefine; /** * encode definition in option. * can be null/undefined. * @type {Objet|HashMap} */ this.encodeDefine = fields.encodeDefine && createHashMap(fields.encodeDefine); /** * Not null/undefined, uint. * @type {number} */ this.startIndex = fields.startIndex || 0; /** * Can be null/undefined (when unknown), uint. * @type {number} */ this.dimensionsDetectCount = fields.dimensionsDetectCount; } /** * Wrap original series data for some compatibility cases. */ Source.seriesDataToSource = function (data) { return new Source({ data: data, sourceFormat: isTypedArray(data) ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL, fromDataset: false }); }; enableClassCheck(Source); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // The result of `guessOrdinal`. var BE_ORDINAL = { Must: 1, // Encounter string but not '-' and not number-like. Might: 2, // Encounter string but number-like. Not: 3 // Other cases }; var inner$3 = makeInner(); /** * @see {module:echarts/data/Source} * @param {module:echarts/component/dataset/DatasetModel} datasetModel * @return {string} sourceFormat */ function detectSourceFormat(datasetModel) { var data = datasetModel.option.source; var sourceFormat = SOURCE_FORMAT_UNKNOWN; if (isTypedArray(data)) { sourceFormat = SOURCE_FORMAT_TYPED_ARRAY; } else if (isArray(data)) { // FIXME Whether tolerate null in top level array? if (data.length === 0) { sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; } for (var i = 0, len = data.length; i < len; i++) { var item = data[i]; if (item == null) { continue; } else if (isArray(item)) { sourceFormat = SOURCE_FORMAT_ARRAY_ROWS; break; } else if (isObject$1(item)) { sourceFormat = SOURCE_FORMAT_OBJECT_ROWS; break; } } } else if (isObject$1(data)) { for (var key in data) { if (data.hasOwnProperty(key) && isArrayLike(data[key])) { sourceFormat = SOURCE_FORMAT_KEYED_COLUMNS; break; } } } else if (data != null) { throw new Error('Invalid data'); } inner$3(datasetModel).sourceFormat = sourceFormat; } /** * [Scenarios]: * (1) Provide source data directly: * series: { * encode: {...}, * dimensions: [...] * seriesLayoutBy: 'row', * data: [[...]] * } * (2) Refer to datasetModel. * series: [{ * encode: {...} * // Ignore datasetIndex means `datasetIndex: 0` * // and the dimensions defination in dataset is used * }, { * encode: {...}, * seriesLayoutBy: 'column', * datasetIndex: 1 * }] * * Get data from series itself or datset. * @return {module:echarts/data/Source} source */ function getSource(seriesModel) { return inner$3(seriesModel).source; } /** * MUST be called before mergeOption of all series. * @param {module:echarts/model/Global} ecModel */ function resetSourceDefaulter(ecModel) { // `datasetMap` is used to make default encode. inner$3(ecModel).datasetMap = createHashMap(); } /** * [Caution]: * MUST be called after series option merged and * before "series.getInitailData()" called. * * [The rule of making default encode]: * Category axis (if exists) alway map to the first dimension. * Each other axis occupies a subsequent dimension. * * [Why make default encode]: * Simplify the typing of encode in option, avoiding the case like that: * series: [{encode: {x: 0, y: 1}}, {encode: {x: 0, y: 2}}, {encode: {x: 0, y: 3}}], * where the "y" have to be manually typed as "1, 2, 3, ...". * * @param {module:echarts/model/Series} seriesModel */ function prepareSource(seriesModel) { var seriesOption = seriesModel.option; var data = seriesOption.data; var sourceFormat = isTypedArray(data) ? SOURCE_FORMAT_TYPED_ARRAY : SOURCE_FORMAT_ORIGINAL; var fromDataset = false; var seriesLayoutBy = seriesOption.seriesLayoutBy; var sourceHeader = seriesOption.sourceHeader; var dimensionsDefine = seriesOption.dimensions; var datasetModel = getDatasetModel(seriesModel); if (datasetModel) { var datasetOption = datasetModel.option; data = datasetOption.source; sourceFormat = inner$3(datasetModel).sourceFormat; fromDataset = true; // These settings from series has higher priority. seriesLayoutBy = seriesLayoutBy || datasetOption.seriesLayoutBy; sourceHeader == null && (sourceHeader = datasetOption.sourceHeader); dimensionsDefine = dimensionsDefine || datasetOption.dimensions; } var completeResult = completeBySourceData( data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine ); inner$3(seriesModel).source = new Source({ data: data, fromDataset: fromDataset, seriesLayoutBy: seriesLayoutBy, sourceFormat: sourceFormat, dimensionsDefine: completeResult.dimensionsDefine, startIndex: completeResult.startIndex, dimensionsDetectCount: completeResult.dimensionsDetectCount, // Note: dataset option does not have `encode`. encodeDefine: seriesOption.encode }); } // return {startIndex, dimensionsDefine, dimensionsCount} function completeBySourceData(data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine) { if (!data) { return {dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine)}; } var dimensionsDetectCount; var startIndex; if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { // Rule: Most of the first line are string: it is header. // Caution: consider a line with 5 string and 1 number, // it still can not be sure it is a head, because the // 5 string may be 5 values of category columns. if (sourceHeader === 'auto' || sourceHeader == null) { arrayRowsTravelFirst(function (val) { // '-' is regarded as null/undefined. if (val != null && val !== '-') { if (isString(val)) { startIndex == null && (startIndex = 1); } else { startIndex = 0; } } // 10 is an experience number, avoid long loop. }, seriesLayoutBy, data, 10); } else { startIndex = sourceHeader ? 1 : 0; } if (!dimensionsDefine && startIndex === 1) { dimensionsDefine = []; arrayRowsTravelFirst(function (val, index) { dimensionsDefine[index] = val != null ? val : ''; }, seriesLayoutBy, data); } dimensionsDetectCount = dimensionsDefine ? dimensionsDefine.length : seriesLayoutBy === SERIES_LAYOUT_BY_ROW ? data.length : data[0] ? data[0].length : null; } else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { if (!dimensionsDefine) { dimensionsDefine = objectRowsCollectDimensions(data); } } else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { if (!dimensionsDefine) { dimensionsDefine = []; each$1(data, function (colArr, key) { dimensionsDefine.push(key); }); } } else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { var value0 = getDataItemValue(data[0]); dimensionsDetectCount = isArray(value0) && value0.length || 1; } else if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { if (__DEV__) { assert$1(!!dimensionsDefine, 'dimensions must be given if data is TypedArray.'); } } return { startIndex: startIndex, dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine), dimensionsDetectCount: dimensionsDetectCount }; } // Consider dimensions defined like ['A', 'price', 'B', 'price', 'C', 'price'], // which is reasonable. But dimension name is duplicated. // Returns undefined or an array contains only object without null/undefiend or string. function normalizeDimensionsDefine(dimensionsDefine) { if (!dimensionsDefine) { // The meaning of null/undefined is different from empty array. return; } var nameMap = createHashMap(); return map(dimensionsDefine, function (item, index) { item = extend({}, isObject$1(item) ? item : {name: item}); // User can set null in dimensions. // We dont auto specify name, othewise a given name may // cause it be refered unexpectedly. if (item.name == null) { return item; } // Also consider number form like 2012. item.name += ''; // User may also specify displayName. // displayName will always exists except user not // specified or dim name is not specified or detected. // (A auto generated dim name will not be used as // displayName). if (item.displayName == null) { item.displayName = item.name; } var exist = nameMap.get(item.name); if (!exist) { nameMap.set(item.name, {count: 1}); } else { item.name += '-' + exist.count++; } return item; }); } function arrayRowsTravelFirst(cb, seriesLayoutBy, data, maxLoop) { maxLoop == null && (maxLoop = Infinity); if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { for (var i = 0; i < data.length && i < maxLoop; i++) { cb(data[i] ? data[i][0] : null, i); } } else { var value0 = data[0] || []; for (var i = 0; i < value0.length && i < maxLoop; i++) { cb(value0[i], i); } } } function objectRowsCollectDimensions(data) { var firstIndex = 0; var obj; while (firstIndex < data.length && !(obj = data[firstIndex++])) {} // jshint ignore: line if (obj) { var dimensions = []; each$1(obj, function (value, key) { dimensions.push(key); }); return dimensions; } } /** * [The strategy of the arrengment of data dimensions for dataset]: * "value way": all axes are non-category axes. So series one by one take * several (the number is coordSysDims.length) dimensions from dataset. * The result of data arrengment of data dimensions like: * | ser0_x | ser0_y | ser1_x | ser1_y | ser2_x | ser2_y | * "category way": at least one axis is category axis. So the the first data * dimension is always mapped to the first category axis and shared by * all of the series. The other data dimensions are taken by series like * "value way" does. * The result of data arrengment of data dimensions like: * | ser_shared_x | ser0_y | ser1_y | ser2_y | * * @param {Array.} coordDimensions [{name: , type: , dimsDef: }, ...] * @param {module:model/Series} seriesModel * @param {module:data/Source} source * @return {Object} encode Never be `null/undefined`. */ function makeSeriesEncodeForAxisCoordSys(coordDimensions, seriesModel, source) { var encode = {}; var datasetModel = getDatasetModel(seriesModel); // Currently only make default when using dataset, util more reqirements occur. if (!datasetModel || !coordDimensions) { return encode; } var encodeItemName = []; var encodeSeriesName = []; var ecModel = seriesModel.ecModel; var datasetMap = inner$3(ecModel).datasetMap; var key = datasetModel.uid + '_' + source.seriesLayoutBy; var baseCategoryDimIndex; var categoryWayValueDimStart; coordDimensions = coordDimensions.slice(); each$1(coordDimensions, function (coordDimInfo, coordDimIdx) { !isObject$1(coordDimInfo) && (coordDimensions[coordDimIdx] = {name: coordDimInfo}); if (coordDimInfo.type === 'ordinal' && baseCategoryDimIndex == null) { baseCategoryDimIndex = coordDimIdx; categoryWayValueDimStart = getDataDimCountOnCoordDim(coordDimensions[coordDimIdx]); } encode[coordDimInfo.name] = []; }); var datasetRecord = datasetMap.get(key) || datasetMap.set(key, {categoryWayDim: categoryWayValueDimStart, valueWayDim: 0}); // TODO // Auto detect first time axis and do arrangement. each$1(coordDimensions, function (coordDimInfo, coordDimIdx) { var coordDimName = coordDimInfo.name; var count = getDataDimCountOnCoordDim(coordDimInfo); // In value way. if (baseCategoryDimIndex == null) { var start = datasetRecord.valueWayDim; pushDim(encode[coordDimName], start, count); pushDim(encodeSeriesName, start, count); datasetRecord.valueWayDim += count; // ??? TODO give a better default series name rule? // especially when encode x y specified. // consider: when mutiple series share one dimension // category axis, series name should better use // the other dimsion name. On the other hand, use // both dimensions name. } // In category way, the first category axis. else if (baseCategoryDimIndex === coordDimIdx) { pushDim(encode[coordDimName], 0, count); pushDim(encodeItemName, 0, count); } // In category way, the other axis. else { var start = datasetRecord.categoryWayDim; pushDim(encode[coordDimName], start, count); pushDim(encodeSeriesName, start, count); datasetRecord.categoryWayDim += count; } }); function pushDim(dimIdxArr, idxFrom, idxCount) { for (var i = 0; i < idxCount; i++) { dimIdxArr.push(idxFrom + i); } } function getDataDimCountOnCoordDim(coordDimInfo) { var dimsDef = coordDimInfo.dimsDef; return dimsDef ? dimsDef.length : 1; } encodeItemName.length && (encode.itemName = encodeItemName); encodeSeriesName.length && (encode.seriesName = encodeSeriesName); return encode; } /** * Work for data like [{name: ..., value: ...}, ...]. * * @param {module:model/Series} seriesModel * @param {module:data/Source} source * @return {Object} encode Never be `null/undefined`. */ function makeSeriesEncodeForNameBased(seriesModel, source, dimCount) { var encode = {}; var datasetModel = getDatasetModel(seriesModel); // Currently only make default when using dataset, util more reqirements occur. if (!datasetModel) { return encode; } var sourceFormat = source.sourceFormat; var dimensionsDefine = source.dimensionsDefine; var potentialNameDimIndex; if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS || sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { each$1(dimensionsDefine, function (dim, idx) { if ((isObject$1(dim) ? dim.name : dim) === 'name') { potentialNameDimIndex = idx; } }); } // idxResult: {v, n}. var idxResult = (function () { var idxRes0 = {}; var idxRes1 = {}; var guessRecords = []; // 5 is an experience value. for (var i = 0, len = Math.min(5, dimCount); i < len; i++) { var guessResult = doGuessOrdinal( source.data, sourceFormat, source.seriesLayoutBy, dimensionsDefine, source.startIndex, i ); guessRecords.push(guessResult); var isPureNumber = guessResult === BE_ORDINAL.Not; // [Strategy of idxRes0]: find the first BE_ORDINAL.Not as the value dim, // and then find a name dim with the priority: // "BE_ORDINAL.Might|BE_ORDINAL.Must" > "other dim" > "the value dim itself". if (isPureNumber && idxRes0.v == null && i !== potentialNameDimIndex) { idxRes0.v = i; } if (idxRes0.n == null || (idxRes0.n === idxRes0.v) || (!isPureNumber && guessRecords[idxRes0.n] === BE_ORDINAL.Not) ) { idxRes0.n = i; } if (fulfilled(idxRes0) && guessRecords[idxRes0.n] !== BE_ORDINAL.Not) { return idxRes0; } // [Strategy of idxRes1]: if idxRes0 not satisfied (that is, no BE_ORDINAL.Not), // find the first BE_ORDINAL.Might as the value dim, // and then find a name dim with the priority: // "other dim" > "the value dim itself". // That is for backward compat: number-like (e.g., `'3'`, `'55'`) can be // treated as number. if (!isPureNumber) { if (guessResult === BE_ORDINAL.Might && idxRes1.v == null && i !== potentialNameDimIndex) { idxRes1.v = i; } if (idxRes1.n == null || (idxRes1.n === idxRes1.v)) { idxRes1.n = i; } } } function fulfilled(idxResult) { return idxResult.v != null && idxResult.n != null; } return fulfilled(idxRes0) ? idxRes0 : fulfilled(idxRes1) ? idxRes1 : null; })(); if (idxResult) { encode.value = idxResult.v; // `potentialNameDimIndex` has highest priority. var nameDimIndex = potentialNameDimIndex != null ? potentialNameDimIndex : idxResult.n; // By default, label use itemName in charts. // So we dont set encodeLabel here. encode.itemName = [nameDimIndex]; encode.seriesName = [nameDimIndex]; } return encode; } /** * If return null/undefined, indicate that should not use datasetModel. */ function getDatasetModel(seriesModel) { var option = seriesModel.option; // Caution: consider the scenario: // A dataset is declared and a series is not expected to use the dataset, // and at the beginning `setOption({series: { noData })` (just prepare other // option but no data), then `setOption({series: {data: [...]}); In this case, // the user should set an empty array to avoid that dataset is used by default. var thisData = option.data; if (!thisData) { return seriesModel.ecModel.getComponent('dataset', option.datasetIndex || 0); } } /** * The rule should not be complex, otherwise user might not * be able to known where the data is wrong. * The code is ugly, but how to make it neat? * * @param {module:echars/data/Source} source * @param {number} dimIndex * @return {BE_ORDINAL} guess result. */ function guessOrdinal(source, dimIndex) { return doGuessOrdinal( source.data, source.sourceFormat, source.seriesLayoutBy, source.dimensionsDefine, source.startIndex, dimIndex ); } // dimIndex may be overflow source data. // return {BE_ORDINAL} function doGuessOrdinal( data, sourceFormat, seriesLayoutBy, dimensionsDefine, startIndex, dimIndex ) { var result; // Experience value. var maxLoop = 5; if (isTypedArray(data)) { return BE_ORDINAL.Not; } // When sourceType is 'objectRows' or 'keyedColumns', dimensionsDefine // always exists in source. var dimName; var dimType; if (dimensionsDefine) { var dimDefItem = dimensionsDefine[dimIndex]; if (isObject$1(dimDefItem)) { dimName = dimDefItem.name; dimType = dimDefItem.type; } else if (isString(dimDefItem)) { dimName = dimDefItem; } } if (dimType != null) { return dimType === 'ordinal' ? BE_ORDINAL.Must : BE_ORDINAL.Not; } if (sourceFormat === SOURCE_FORMAT_ARRAY_ROWS) { if (seriesLayoutBy === SERIES_LAYOUT_BY_ROW) { var sample = data[dimIndex]; for (var i = 0; i < (sample || []).length && i < maxLoop; i++) { if ((result = detectValue(sample[startIndex + i])) != null) { return result; } } } else { for (var i = 0; i < data.length && i < maxLoop; i++) { var row = data[startIndex + i]; if (row && (result = detectValue(row[dimIndex])) != null) { return result; } } } } else if (sourceFormat === SOURCE_FORMAT_OBJECT_ROWS) { if (!dimName) { return BE_ORDINAL.Not; } for (var i = 0; i < data.length && i < maxLoop; i++) { var item = data[i]; if (item && (result = detectValue(item[dimName])) != null) { return result; } } } else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { if (!dimName) { return BE_ORDINAL.Not; } var sample = data[dimName]; if (!sample || isTypedArray(sample)) { return BE_ORDINAL.Not; } for (var i = 0; i < sample.length && i < maxLoop; i++) { if ((result = detectValue(sample[i])) != null) { return result; } } } else if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { for (var i = 0; i < data.length && i < maxLoop; i++) { var item = data[i]; var val = getDataItemValue(item); if (!isArray(val)) { return BE_ORDINAL.Not; } if ((result = detectValue(val[dimIndex])) != null) { return result; } } } function detectValue(val) { var beStr = isString(val); // Consider usage convenience, '1', '2' will be treated as "number". // `isFinit('')` get `true`. if (val != null && isFinite(val) && val !== '') { return beStr ? BE_ORDINAL.Might : BE_ORDINAL.Not; } else if (beStr && val !== '-') { return BE_ORDINAL.Must; } } return BE_ORDINAL.Not; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * ECharts global model * * @module {echarts/model/Global} */ /** * Caution: If the mechanism should be changed some day, these cases * should be considered: * * (1) In `merge option` mode, if using the same option to call `setOption` * many times, the result should be the same (try our best to ensure that). * (2) In `merge option` mode, if a component has no id/name specified, it * will be merged by index, and the result sequence of the components is * consistent to the original sequence. * (3) `reset` feature (in toolbox). Find detailed info in comments about * `mergeOption` in module:echarts/model/OptionManager. */ var OPTION_INNER_KEY = '\0_ec_inner'; /** * @alias module:echarts/model/Global * * @param {Object} option * @param {module:echarts/model/Model} parentModel * @param {Object} theme */ var GlobalModel = Model.extend({ init: function (option, parentModel, theme, optionManager) { theme = theme || {}; this.option = null; // Mark as not initialized. /** * @type {module:echarts/model/Model} * @private */ this._theme = new Model(theme); /** * @type {module:echarts/model/OptionManager} */ this._optionManager = optionManager; }, setOption: function (option, optionPreprocessorFuncs) { assert$1( !(OPTION_INNER_KEY in option), 'please use chart.getOption()' ); this._optionManager.setOption(option, optionPreprocessorFuncs); this.resetOption(null); }, /** * @param {string} type null/undefined: reset all. * 'recreate': force recreate all. * 'timeline': only reset timeline option * 'media': only reset media query option * @return {boolean} Whether option changed. */ resetOption: function (type) { var optionChanged = false; var optionManager = this._optionManager; if (!type || type === 'recreate') { var baseOption = optionManager.mountOption(type === 'recreate'); if (!this.option || type === 'recreate') { initBase.call(this, baseOption); } else { this.restoreData(); this.mergeOption(baseOption); } optionChanged = true; } if (type === 'timeline' || type === 'media') { this.restoreData(); } if (!type || type === 'recreate' || type === 'timeline') { var timelineOption = optionManager.getTimelineOption(this); timelineOption && (this.mergeOption(timelineOption), optionChanged = true); } if (!type || type === 'recreate' || type === 'media') { var mediaOptions = optionManager.getMediaOption(this, this._api); if (mediaOptions.length) { each$1(mediaOptions, function (mediaOption) { this.mergeOption(mediaOption, optionChanged = true); }, this); } } return optionChanged; }, /** * @protected */ mergeOption: function (newOption) { var option = this.option; var componentsMap = this._componentsMap; var newCptTypes = []; resetSourceDefaulter(this); // If no component class, merge directly. // For example: color, animaiton options, etc. each$1(newOption, function (componentOption, mainType) { if (componentOption == null) { return; } if (!ComponentModel.hasClass(mainType)) { // globalSettingTask.dirty(); option[mainType] = option[mainType] == null ? clone(componentOption) : merge(option[mainType], componentOption, true); } else if (mainType) { newCptTypes.push(mainType); } }); ComponentModel.topologicalTravel( newCptTypes, ComponentModel.getAllClassMainTypes(), visitComponent, this ); function visitComponent(mainType, dependencies) { var newCptOptionList = normalizeToArray(newOption[mainType]); var mapResult = mappingToExists( componentsMap.get(mainType), newCptOptionList ); makeIdAndName(mapResult); // Set mainType and complete subType. each$1(mapResult, function (item, index) { var opt = item.option; if (isObject$1(opt)) { item.keyInfo.mainType = mainType; item.keyInfo.subType = determineSubType(mainType, opt, item.exist); } }); var dependentModels = getComponentsByTypes( componentsMap, dependencies ); option[mainType] = []; componentsMap.set(mainType, []); each$1(mapResult, function (resultItem, index) { var componentModel = resultItem.exist; var newCptOption = resultItem.option; assert$1( isObject$1(newCptOption) || componentModel, 'Empty component definition' ); // Consider where is no new option and should be merged using {}, // see removeEdgeAndAdd in topologicalTravel and // ComponentModel.getAllClassMainTypes. if (!newCptOption) { componentModel.mergeOption({}, this); componentModel.optionUpdated({}, false); } else { var ComponentModelClass = ComponentModel.getClass( mainType, resultItem.keyInfo.subType, true ); if (componentModel && componentModel.constructor === ComponentModelClass) { componentModel.name = resultItem.keyInfo.name; // componentModel.settingTask && componentModel.settingTask.dirty(); componentModel.mergeOption(newCptOption, this); componentModel.optionUpdated(newCptOption, false); } else { // PENDING Global as parent ? var extraOpt = extend( { dependentModels: dependentModels, componentIndex: index }, resultItem.keyInfo ); componentModel = new ComponentModelClass( newCptOption, this, this, extraOpt ); extend(componentModel, extraOpt); componentModel.init(newCptOption, this, this, extraOpt); // Call optionUpdated after init. // newCptOption has been used as componentModel.option // and may be merged with theme and default, so pass null // to avoid confusion. componentModel.optionUpdated(null, true); } } componentsMap.get(mainType)[index] = componentModel; option[mainType][index] = componentModel.option; }, this); // Backup series for filtering. if (mainType === 'series') { createSeriesIndices(this, componentsMap.get('series')); } } this._seriesIndicesMap = createHashMap( this._seriesIndices = this._seriesIndices || [] ); }, /** * Get option for output (cloned option and inner info removed) * @public * @return {Object} */ getOption: function () { var option = clone(this.option); each$1(option, function (opts, mainType) { if (ComponentModel.hasClass(mainType)) { var opts = normalizeToArray(opts); for (var i = opts.length - 1; i >= 0; i--) { // Remove options with inner id. if (isIdInner(opts[i])) { opts.splice(i, 1); } } option[mainType] = opts; } }); delete option[OPTION_INNER_KEY]; return option; }, /** * @return {module:echarts/model/Model} */ getTheme: function () { return this._theme; }, /** * @param {string} mainType * @param {number} [idx=0] * @return {module:echarts/model/Component} */ getComponent: function (mainType, idx) { var list = this._componentsMap.get(mainType); if (list) { return list[idx || 0]; } }, /** * If none of index and id and name used, return all components with mainType. * @param {Object} condition * @param {string} condition.mainType * @param {string} [condition.subType] If ignore, only query by mainType * @param {number|Array.} [condition.index] Either input index or id or name. * @param {string|Array.} [condition.id] Either input index or id or name. * @param {string|Array.} [condition.name] Either input index or id or name. * @return {Array.} */ queryComponents: function (condition) { var mainType = condition.mainType; if (!mainType) { return []; } var index = condition.index; var id = condition.id; var name = condition.name; var cpts = this._componentsMap.get(mainType); if (!cpts || !cpts.length) { return []; } var result; if (index != null) { if (!isArray(index)) { index = [index]; } result = filter(map(index, function (idx) { return cpts[idx]; }), function (val) { return !!val; }); } else if (id != null) { var isIdArray = isArray(id); result = filter(cpts, function (cpt) { return (isIdArray && indexOf(id, cpt.id) >= 0) || (!isIdArray && cpt.id === id); }); } else if (name != null) { var isNameArray = isArray(name); result = filter(cpts, function (cpt) { return (isNameArray && indexOf(name, cpt.name) >= 0) || (!isNameArray && cpt.name === name); }); } else { // Return all components with mainType result = cpts.slice(); } return filterBySubType(result, condition); }, /** * The interface is different from queryComponents, * which is convenient for inner usage. * * @usage * var result = findComponents( * {mainType: 'dataZoom', query: {dataZoomId: 'abc'}} * ); * var result = findComponents( * {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}} * ); * var result = findComponents( * {mainType: 'series', * filter: function (model, index) {...}} * ); * // result like [component0, componnet1, ...] * * @param {Object} condition * @param {string} condition.mainType Mandatory. * @param {string} [condition.subType] Optional. * @param {Object} [condition.query] like {xxxIndex, xxxId, xxxName}, * where xxx is mainType. * If query attribute is null/undefined or has no index/id/name, * do not filtering by query conditions, which is convenient for * no-payload situations or when target of action is global. * @param {Function} [condition.filter] parameter: component, return boolean. * @return {Array.} */ findComponents: function (condition) { var query = condition.query; var mainType = condition.mainType; var queryCond = getQueryCond(query); var result = queryCond ? this.queryComponents(queryCond) : this._componentsMap.get(mainType); return doFilter(filterBySubType(result, condition)); function getQueryCond(q) { var indexAttr = mainType + 'Index'; var idAttr = mainType + 'Id'; var nameAttr = mainType + 'Name'; return q && ( q[indexAttr] != null || q[idAttr] != null || q[nameAttr] != null ) ? { mainType: mainType, // subType will be filtered finally. index: q[indexAttr], id: q[idAttr], name: q[nameAttr] } : null; } function doFilter(res) { return condition.filter ? filter(res, condition.filter) : res; } }, /** * @usage * eachComponent('legend', function (legendModel, index) { * ... * }); * eachComponent(function (componentType, model, index) { * // componentType does not include subType * // (componentType is 'xxx' but not 'xxx.aa') * }); * eachComponent( * {mainType: 'dataZoom', query: {dataZoomId: 'abc'}}, * function (model, index) {...} * ); * eachComponent( * {mainType: 'series', subType: 'pie', query: {seriesName: 'uio'}}, * function (model, index) {...} * ); * * @param {string|Object=} mainType When mainType is object, the definition * is the same as the method 'findComponents'. * @param {Function} cb * @param {*} context */ eachComponent: function (mainType, cb, context) { var componentsMap = this._componentsMap; if (typeof mainType === 'function') { context = cb; cb = mainType; componentsMap.each(function (components, componentType) { each$1(components, function (component, index) { cb.call(context, componentType, component, index); }); }); } else if (isString(mainType)) { each$1(componentsMap.get(mainType), cb, context); } else if (isObject$1(mainType)) { var queryResult = this.findComponents(mainType); each$1(queryResult, cb, context); } }, /** * @param {string} name * @return {Array.} */ getSeriesByName: function (name) { var series = this._componentsMap.get('series'); return filter(series, function (oneSeries) { return oneSeries.name === name; }); }, /** * @param {number} seriesIndex * @return {module:echarts/model/Series} */ getSeriesByIndex: function (seriesIndex) { return this._componentsMap.get('series')[seriesIndex]; }, /** * Get series list before filtered by type. * FIXME: rename to getRawSeriesByType? * * @param {string} subType * @return {Array.} */ getSeriesByType: function (subType) { var series = this._componentsMap.get('series'); return filter(series, function (oneSeries) { return oneSeries.subType === subType; }); }, /** * @return {Array.} */ getSeries: function () { return this._componentsMap.get('series').slice(); }, /** * @return {number} */ getSeriesCount: function () { return this._componentsMap.get('series').length; }, /** * After filtering, series may be different * frome raw series. * * @param {Function} cb * @param {*} context */ eachSeries: function (cb, context) { assertSeriesInitialized(this); each$1(this._seriesIndices, function (rawSeriesIndex) { var series = this._componentsMap.get('series')[rawSeriesIndex]; cb.call(context, series, rawSeriesIndex); }, this); }, /** * Iterate raw series before filtered. * * @param {Function} cb * @param {*} context */ eachRawSeries: function (cb, context) { each$1(this._componentsMap.get('series'), cb, context); }, /** * After filtering, series may be different. * frome raw series. * * @param {string} subType. * @param {Function} cb * @param {*} context */ eachSeriesByType: function (subType, cb, context) { assertSeriesInitialized(this); each$1(this._seriesIndices, function (rawSeriesIndex) { var series = this._componentsMap.get('series')[rawSeriesIndex]; if (series.subType === subType) { cb.call(context, series, rawSeriesIndex); } }, this); }, /** * Iterate raw series before filtered of given type. * * @parma {string} subType * @param {Function} cb * @param {*} context */ eachRawSeriesByType: function (subType, cb, context) { return each$1(this.getSeriesByType(subType), cb, context); }, /** * @param {module:echarts/model/Series} seriesModel */ isSeriesFiltered: function (seriesModel) { assertSeriesInitialized(this); return this._seriesIndicesMap.get(seriesModel.componentIndex) == null; }, /** * @return {Array.} */ getCurrentSeriesIndices: function () { return (this._seriesIndices || []).slice(); }, /** * @param {Function} cb * @param {*} context */ filterSeries: function (cb, context) { assertSeriesInitialized(this); var filteredSeries = filter( this._componentsMap.get('series'), cb, context ); createSeriesIndices(this, filteredSeries); }, restoreData: function (payload) { var componentsMap = this._componentsMap; createSeriesIndices(this, componentsMap.get('series')); var componentTypes = []; componentsMap.each(function (components, componentType) { componentTypes.push(componentType); }); ComponentModel.topologicalTravel( componentTypes, ComponentModel.getAllClassMainTypes(), function (componentType, dependencies) { each$1(componentsMap.get(componentType), function (component) { (componentType !== 'series' || !isNotTargetSeries(component, payload)) && component.restoreData(); }); } ); } }); function isNotTargetSeries(seriesModel, payload) { if (payload) { var index = payload.seiresIndex; var id = payload.seriesId; var name = payload.seriesName; return (index != null && seriesModel.componentIndex !== index) || (id != null && seriesModel.id !== id) || (name != null && seriesModel.name !== name); } } /** * @inner */ function mergeTheme(option, theme) { // PENDING // NOT use `colorLayer` in theme if option has `color` var notMergeColorLayer = option.color && !option.colorLayer; each$1(theme, function (themeItem, name) { if (name === 'colorLayer' && notMergeColorLayer) { return; } // 如果有 component model 则把具体的 merge 逻辑交给该 model 处理 if (!ComponentModel.hasClass(name)) { if (typeof themeItem === 'object') { option[name] = !option[name] ? clone(themeItem) : merge(option[name], themeItem, false); } else { if (option[name] == null) { option[name] = themeItem; } } } }); } function initBase(baseOption) { baseOption = baseOption; // Using OPTION_INNER_KEY to mark that this option can not be used outside, // i.e. `chart.setOption(chart.getModel().option);` is forbiden. this.option = {}; this.option[OPTION_INNER_KEY] = 1; /** * Init with series: [], in case of calling findSeries method * before series initialized. * @type {Object.>} * @private */ this._componentsMap = createHashMap({series: []}); /** * Mapping between filtered series list and raw series list. * key: filtered series indices, value: raw series indices. * @type {Array.} * @private */ this._seriesIndices; this._seriesIndicesMap; mergeTheme(baseOption, this._theme.option); // TODO Needs clone when merging to the unexisted property merge(baseOption, globalDefault, false); this.mergeOption(baseOption); } /** * @inner * @param {Array.|string} types model types * @return {Object} key: {string} type, value: {Array.} models */ function getComponentsByTypes(componentsMap, types) { if (!isArray(types)) { types = types ? [types] : []; } var ret = {}; each$1(types, function (type) { ret[type] = (componentsMap.get(type) || []).slice(); }); return ret; } /** * @inner */ function determineSubType(mainType, newCptOption, existComponent) { var subType = newCptOption.type ? newCptOption.type : existComponent ? existComponent.subType // Use determineSubType only when there is no existComponent. : ComponentModel.determineSubType(mainType, newCptOption); // tooltip, markline, markpoint may always has no subType return subType; } /** * @inner */ function createSeriesIndices(ecModel, seriesModels) { ecModel._seriesIndicesMap = createHashMap( ecModel._seriesIndices = map(seriesModels, function (series) { return series.componentIndex; }) || [] ); } /** * @inner */ function filterBySubType(components, condition) { // Using hasOwnProperty for restrict. Consider // subType is undefined in user payload. return condition.hasOwnProperty('subType') ? filter(components, function (cpt) { return cpt.subType === condition.subType; }) : components; } /** * @inner */ function assertSeriesInitialized(ecModel) { // Components that use _seriesIndices should depends on series component, // which make sure that their initialization is after series. if (__DEV__) { if (!ecModel._seriesIndices) { throw new Error('Option should contains series.'); } } } mixin(GlobalModel, colorPaletteMixin); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var echartsAPIList = [ 'getDom', 'getZr', 'getWidth', 'getHeight', 'getDevicePixelRatio', 'dispatchAction', 'isDisposed', 'on', 'off', 'getDataURL', 'getConnectedDataURL', 'getModel', 'getOption', 'getViewOfComponentModel', 'getViewOfSeriesModel' ]; // And `getCoordinateSystems` and `getComponentByElement` will be injected in echarts.js function ExtensionAPI(chartInstance) { each$1(echartsAPIList, function (name) { this[name] = bind(chartInstance[name], chartInstance); }, this); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var coordinateSystemCreators = {}; function CoordinateSystemManager() { this._coordinateSystems = []; } CoordinateSystemManager.prototype = { constructor: CoordinateSystemManager, create: function (ecModel, api) { var coordinateSystems = []; each$1(coordinateSystemCreators, function (creater, type) { var list = creater.create(ecModel, api); coordinateSystems = coordinateSystems.concat(list || []); }); this._coordinateSystems = coordinateSystems; }, update: function (ecModel, api) { each$1(this._coordinateSystems, function (coordSys) { coordSys.update && coordSys.update(ecModel, api); }); }, getCoordinateSystems: function () { return this._coordinateSystems.slice(); } }; CoordinateSystemManager.register = function (type, coordinateSystemCreator) { coordinateSystemCreators[type] = coordinateSystemCreator; }; CoordinateSystemManager.get = function (type) { return coordinateSystemCreators[type]; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * ECharts option manager * * @module {echarts/model/OptionManager} */ var each$4 = each$1; var clone$3 = clone; var map$1 = map; var merge$1 = merge; var QUERY_REG = /^(min|max)?(.+)$/; /** * TERM EXPLANATIONS: * * [option]: * * An object that contains definitions of components. For example: * var option = { * title: {...}, * legend: {...}, * visualMap: {...}, * series: [ * {data: [...]}, * {data: [...]}, * ... * ] * }; * * [rawOption]: * * An object input to echarts.setOption. 'rawOption' may be an * 'option', or may be an object contains multi-options. For example: * var option = { * baseOption: { * title: {...}, * legend: {...}, * series: [ * {data: [...]}, * {data: [...]}, * ... * ] * }, * timeline: {...}, * options: [ * {title: {...}, series: {data: [...]}}, * {title: {...}, series: {data: [...]}}, * ... * ], * media: [ * { * query: {maxWidth: 320}, * option: {series: {x: 20}, visualMap: {show: false}} * }, * { * query: {minWidth: 320, maxWidth: 720}, * option: {series: {x: 500}, visualMap: {show: true}} * }, * { * option: {series: {x: 1200}, visualMap: {show: true}} * } * ] * }; * * @alias module:echarts/model/OptionManager * @param {module:echarts/ExtensionAPI} api */ function OptionManager(api) { /** * @private * @type {module:echarts/ExtensionAPI} */ this._api = api; /** * @private * @type {Array.} */ this._timelineOptions = []; /** * @private * @type {Array.} */ this._mediaList = []; /** * @private * @type {Object} */ this._mediaDefault; /** * -1, means default. * empty means no media. * @private * @type {Array.} */ this._currentMediaIndices = []; /** * @private * @type {Object} */ this._optionBackup; /** * @private * @type {Object} */ this._newBaseOption; } // timeline.notMerge is not supported in ec3. Firstly there is rearly // case that notMerge is needed. Secondly supporting 'notMerge' requires // rawOption cloned and backuped when timeline changed, which does no // good to performance. What's more, that both timeline and setOption // method supply 'notMerge' brings complex and some problems. // Consider this case: // (step1) chart.setOption({timeline: {notMerge: false}, ...}, false); // (step2) chart.setOption({timeline: {notMerge: true}, ...}, false); OptionManager.prototype = { constructor: OptionManager, /** * @public * @param {Object} rawOption Raw option. * @param {module:echarts/model/Global} ecModel * @param {Array.} optionPreprocessorFuncs * @return {Object} Init option */ setOption: function (rawOption, optionPreprocessorFuncs) { if (rawOption) { // That set dat primitive is dangerous if user reuse the data when setOption again. each$1(normalizeToArray(rawOption.series), function (series) { series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data); }); } // Caution: some series modify option data, if do not clone, // it should ensure that the repeat modify correctly // (create a new object when modify itself). rawOption = clone$3(rawOption); // FIXME // 如果 timeline options 或者 media 中设置了某个属性,而baseOption中没有设置,则进行警告。 var oldOptionBackup = this._optionBackup; var newParsedOption = parseRawOption.call( this, rawOption, optionPreprocessorFuncs, !oldOptionBackup ); this._newBaseOption = newParsedOption.baseOption; // For setOption at second time (using merge mode); if (oldOptionBackup) { // Only baseOption can be merged. mergeOption(oldOptionBackup.baseOption, newParsedOption.baseOption); // For simplicity, timeline options and media options do not support merge, // that is, if you `setOption` twice and both has timeline options, the latter // timeline opitons will not be merged to the formers, but just substitude them. if (newParsedOption.timelineOptions.length) { oldOptionBackup.timelineOptions = newParsedOption.timelineOptions; } if (newParsedOption.mediaList.length) { oldOptionBackup.mediaList = newParsedOption.mediaList; } if (newParsedOption.mediaDefault) { oldOptionBackup.mediaDefault = newParsedOption.mediaDefault; } } else { this._optionBackup = newParsedOption; } }, /** * @param {boolean} isRecreate * @return {Object} */ mountOption: function (isRecreate) { var optionBackup = this._optionBackup; // TODO // 如果没有reset功能则不clone。 this._timelineOptions = map$1(optionBackup.timelineOptions, clone$3); this._mediaList = map$1(optionBackup.mediaList, clone$3); this._mediaDefault = clone$3(optionBackup.mediaDefault); this._currentMediaIndices = []; return clone$3(isRecreate // this._optionBackup.baseOption, which is created at the first `setOption` // called, and is merged into every new option by inner method `mergeOption` // each time `setOption` called, can be only used in `isRecreate`, because // its reliability is under suspicion. In other cases option merge is // performed by `model.mergeOption`. ? optionBackup.baseOption : this._newBaseOption ); }, /** * @param {module:echarts/model/Global} ecModel * @return {Object} */ getTimelineOption: function (ecModel) { var option; var timelineOptions = this._timelineOptions; if (timelineOptions.length) { // getTimelineOption can only be called after ecModel inited, // so we can get currentIndex from timelineModel. var timelineModel = ecModel.getComponent('timeline'); if (timelineModel) { option = clone$3( timelineOptions[timelineModel.getCurrentIndex()], true ); } } return option; }, /** * @param {module:echarts/model/Global} ecModel * @return {Array.} */ getMediaOption: function (ecModel) { var ecWidth = this._api.getWidth(); var ecHeight = this._api.getHeight(); var mediaList = this._mediaList; var mediaDefault = this._mediaDefault; var indices = []; var result = []; // No media defined. if (!mediaList.length && !mediaDefault) { return result; } // Multi media may be applied, the latter defined media has higher priority. for (var i = 0, len = mediaList.length; i < len; i++) { if (applyMediaQuery(mediaList[i].query, ecWidth, ecHeight)) { indices.push(i); } } // FIXME // 是否mediaDefault应该强制用户设置,否则可能修改不能回归。 if (!indices.length && mediaDefault) { indices = [-1]; } if (indices.length && !indicesEquals(indices, this._currentMediaIndices)) { result = map$1(indices, function (index) { return clone$3( index === -1 ? mediaDefault.option : mediaList[index].option ); }); } // Otherwise return nothing. this._currentMediaIndices = indices; return result; } }; function parseRawOption(rawOption, optionPreprocessorFuncs, isNew) { var timelineOptions = []; var mediaList = []; var mediaDefault; var baseOption; // Compatible with ec2. var timelineOpt = rawOption.timeline; if (rawOption.baseOption) { baseOption = rawOption.baseOption; } // For timeline if (timelineOpt || rawOption.options) { baseOption = baseOption || {}; timelineOptions = (rawOption.options || []).slice(); } // For media query if (rawOption.media) { baseOption = baseOption || {}; var media = rawOption.media; each$4(media, function (singleMedia) { if (singleMedia && singleMedia.option) { if (singleMedia.query) { mediaList.push(singleMedia); } else if (!mediaDefault) { // Use the first media default. mediaDefault = singleMedia; } } }); } // For normal option if (!baseOption) { baseOption = rawOption; } // Set timelineOpt to baseOption in ec3, // which is convenient for merge option. if (!baseOption.timeline) { baseOption.timeline = timelineOpt; } // Preprocess. each$4([baseOption].concat(timelineOptions) .concat(map(mediaList, function (media) { return media.option; })), function (option) { each$4(optionPreprocessorFuncs, function (preProcess) { preProcess(option, isNew); }); } ); return { baseOption: baseOption, timelineOptions: timelineOptions, mediaDefault: mediaDefault, mediaList: mediaList }; } /** * @see * Support: width, height, aspectRatio * Can use max or min as prefix. */ function applyMediaQuery(query, ecWidth, ecHeight) { var realMap = { width: ecWidth, height: ecHeight, aspectratio: ecWidth / ecHeight // lowser case for convenientce. }; var applicatable = true; each$1(query, function (value, attr) { var matched = attr.match(QUERY_REG); if (!matched || !matched[1] || !matched[2]) { return; } var operator = matched[1]; var realAttr = matched[2].toLowerCase(); if (!compare(realMap[realAttr], value, operator)) { applicatable = false; } }); return applicatable; } function compare(real, expect, operator) { if (operator === 'min') { return real >= expect; } else if (operator === 'max') { return real <= expect; } else { // Equals return real === expect; } } function indicesEquals(indices1, indices2) { // indices is always order by asc and has only finite number. return indices1.join(',') === indices2.join(','); } /** * Consider case: * `chart.setOption(opt1);` * Then user do some interaction like dataZoom, dataView changing. * `chart.setOption(opt2);` * Then user press 'reset button' in toolbox. * * After doing that all of the interaction effects should be reset, the * chart should be the same as the result of invoke * `chart.setOption(opt1); chart.setOption(opt2);`. * * Although it is not able ensure that * `chart.setOption(opt1); chart.setOption(opt2);` is equivalents to * `chart.setOption(merge(opt1, opt2));` exactly, * this might be the only simple way to implement that feature. * * MEMO: We've considered some other approaches: * 1. Each model handle its self restoration but not uniform treatment. * (Too complex in logic and error-prone) * 2. Use a shadow ecModel. (Performace expensive) */ function mergeOption(oldOption, newOption) { newOption = newOption || {}; each$4(newOption, function (newCptOpt, mainType) { if (newCptOpt == null) { return; } var oldCptOpt = oldOption[mainType]; if (!ComponentModel.hasClass(mainType)) { oldOption[mainType] = merge$1(oldCptOpt, newCptOpt, true); } else { newCptOpt = normalizeToArray(newCptOpt); oldCptOpt = normalizeToArray(oldCptOpt); var mapResult = mappingToExists(oldCptOpt, newCptOpt); oldOption[mainType] = map$1(mapResult, function (item) { return (item.option && item.exist) ? merge$1(item.exist, item.option, true) : (item.exist || item.option); }); } }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$5 = each$1; var isObject$3 = isObject$1; var POSSIBLE_STYLES = [ 'areaStyle', 'lineStyle', 'nodeStyle', 'linkStyle', 'chordStyle', 'label', 'labelLine' ]; function compatEC2ItemStyle(opt) { var itemStyleOpt = opt && opt.itemStyle; if (!itemStyleOpt) { return; } for (var i = 0, len = POSSIBLE_STYLES.length; i < len; i++) { var styleName = POSSIBLE_STYLES[i]; var normalItemStyleOpt = itemStyleOpt.normal; var emphasisItemStyleOpt = itemStyleOpt.emphasis; if (normalItemStyleOpt && normalItemStyleOpt[styleName]) { opt[styleName] = opt[styleName] || {}; if (!opt[styleName].normal) { opt[styleName].normal = normalItemStyleOpt[styleName]; } else { merge(opt[styleName].normal, normalItemStyleOpt[styleName]); } normalItemStyleOpt[styleName] = null; } if (emphasisItemStyleOpt && emphasisItemStyleOpt[styleName]) { opt[styleName] = opt[styleName] || {}; if (!opt[styleName].emphasis) { opt[styleName].emphasis = emphasisItemStyleOpt[styleName]; } else { merge(opt[styleName].emphasis, emphasisItemStyleOpt[styleName]); } emphasisItemStyleOpt[styleName] = null; } } } function convertNormalEmphasis(opt, optType, useExtend) { if (opt && opt[optType] && (opt[optType].normal || opt[optType].emphasis)) { var normalOpt = opt[optType].normal; var emphasisOpt = opt[optType].emphasis; if (normalOpt) { // Timeline controlStyle has other properties besides normal and emphasis if (useExtend) { opt[optType].normal = opt[optType].emphasis = null; defaults(opt[optType], normalOpt); } else { opt[optType] = normalOpt; } } if (emphasisOpt) { opt.emphasis = opt.emphasis || {}; opt.emphasis[optType] = emphasisOpt; } } } function removeEC3NormalStatus(opt) { convertNormalEmphasis(opt, 'itemStyle'); convertNormalEmphasis(opt, 'lineStyle'); convertNormalEmphasis(opt, 'areaStyle'); convertNormalEmphasis(opt, 'label'); convertNormalEmphasis(opt, 'labelLine'); // treemap convertNormalEmphasis(opt, 'upperLabel'); // graph convertNormalEmphasis(opt, 'edgeLabel'); } function compatTextStyle(opt, propName) { // Check whether is not object (string\null\undefined ...) var labelOptSingle = isObject$3(opt) && opt[propName]; var textStyle = isObject$3(labelOptSingle) && labelOptSingle.textStyle; if (textStyle) { for (var i = 0, len = TEXT_STYLE_OPTIONS.length; i < len; i++) { var propName = TEXT_STYLE_OPTIONS[i]; if (textStyle.hasOwnProperty(propName)) { labelOptSingle[propName] = textStyle[propName]; } } } } function compatEC3CommonStyles(opt) { if (opt) { removeEC3NormalStatus(opt); compatTextStyle(opt, 'label'); opt.emphasis && compatTextStyle(opt.emphasis, 'label'); } } function processSeries(seriesOpt) { if (!isObject$3(seriesOpt)) { return; } compatEC2ItemStyle(seriesOpt); removeEC3NormalStatus(seriesOpt); compatTextStyle(seriesOpt, 'label'); // treemap compatTextStyle(seriesOpt, 'upperLabel'); // graph compatTextStyle(seriesOpt, 'edgeLabel'); if (seriesOpt.emphasis) { compatTextStyle(seriesOpt.emphasis, 'label'); // treemap compatTextStyle(seriesOpt.emphasis, 'upperLabel'); // graph compatTextStyle(seriesOpt.emphasis, 'edgeLabel'); } var markPoint = seriesOpt.markPoint; if (markPoint) { compatEC2ItemStyle(markPoint); compatEC3CommonStyles(markPoint); } var markLine = seriesOpt.markLine; if (markLine) { compatEC2ItemStyle(markLine); compatEC3CommonStyles(markLine); } var markArea = seriesOpt.markArea; if (markArea) { compatEC3CommonStyles(markArea); } var data = seriesOpt.data; // Break with ec3: if `setOption` again, there may be no `type` in option, // then the backward compat based on option type will not be performed. if (seriesOpt.type === 'graph') { data = data || seriesOpt.nodes; var edgeData = seriesOpt.links || seriesOpt.edges; if (edgeData && !isTypedArray(edgeData)) { for (var i = 0; i < edgeData.length; i++) { compatEC3CommonStyles(edgeData[i]); } } each$1(seriesOpt.categories, function (opt) { removeEC3NormalStatus(opt); }); } if (data && !isTypedArray(data)) { for (var i = 0; i < data.length; i++) { compatEC3CommonStyles(data[i]); } } // mark point data var markPoint = seriesOpt.markPoint; if (markPoint && markPoint.data) { var mpData = markPoint.data; for (var i = 0; i < mpData.length; i++) { compatEC3CommonStyles(mpData[i]); } } // mark line data var markLine = seriesOpt.markLine; if (markLine && markLine.data) { var mlData = markLine.data; for (var i = 0; i < mlData.length; i++) { if (isArray(mlData[i])) { compatEC3CommonStyles(mlData[i][0]); compatEC3CommonStyles(mlData[i][1]); } else { compatEC3CommonStyles(mlData[i]); } } } // Series if (seriesOpt.type === 'gauge') { compatTextStyle(seriesOpt, 'axisLabel'); compatTextStyle(seriesOpt, 'title'); compatTextStyle(seriesOpt, 'detail'); } else if (seriesOpt.type === 'treemap') { convertNormalEmphasis(seriesOpt.breadcrumb, 'itemStyle'); each$1(seriesOpt.levels, function (opt) { removeEC3NormalStatus(opt); }); } else if (seriesOpt.type === 'tree') { removeEC3NormalStatus(seriesOpt.leaves); } // sunburst starts from ec4, so it does not need to compat levels. } function toArr(o) { return isArray(o) ? o : o ? [o] : []; } function toObj(o) { return (isArray(o) ? o[0] : o) || {}; } var compatStyle = function (option, isTheme) { each$5(toArr(option.series), function (seriesOpt) { isObject$3(seriesOpt) && processSeries(seriesOpt); }); var axes = ['xAxis', 'yAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'parallelAxis', 'radar']; isTheme && axes.push('valueAxis', 'categoryAxis', 'logAxis', 'timeAxis'); each$5( axes, function (axisName) { each$5(toArr(option[axisName]), function (axisOpt) { if (axisOpt) { compatTextStyle(axisOpt, 'axisLabel'); compatTextStyle(axisOpt.axisPointer, 'label'); } }); } ); each$5(toArr(option.parallel), function (parallelOpt) { var parallelAxisDefault = parallelOpt && parallelOpt.parallelAxisDefault; compatTextStyle(parallelAxisDefault, 'axisLabel'); compatTextStyle(parallelAxisDefault && parallelAxisDefault.axisPointer, 'label'); }); each$5(toArr(option.calendar), function (calendarOpt) { convertNormalEmphasis(calendarOpt, 'itemStyle'); compatTextStyle(calendarOpt, 'dayLabel'); compatTextStyle(calendarOpt, 'monthLabel'); compatTextStyle(calendarOpt, 'yearLabel'); }); // radar.name.textStyle each$5(toArr(option.radar), function (radarOpt) { compatTextStyle(radarOpt, 'name'); }); each$5(toArr(option.geo), function (geoOpt) { if (isObject$3(geoOpt)) { compatEC3CommonStyles(geoOpt); each$5(toArr(geoOpt.regions), function (regionObj) { compatEC3CommonStyles(regionObj); }); } }); each$5(toArr(option.timeline), function (timelineOpt) { compatEC3CommonStyles(timelineOpt); convertNormalEmphasis(timelineOpt, 'label'); convertNormalEmphasis(timelineOpt, 'itemStyle'); convertNormalEmphasis(timelineOpt, 'controlStyle', true); var data = timelineOpt.data; isArray(data) && each$1(data, function (item) { if (isObject$1(item)) { convertNormalEmphasis(item, 'label'); convertNormalEmphasis(item, 'itemStyle'); } }); }); each$5(toArr(option.toolbox), function (toolboxOpt) { convertNormalEmphasis(toolboxOpt, 'iconStyle'); each$5(toolboxOpt.feature, function (featureOpt) { convertNormalEmphasis(featureOpt, 'iconStyle'); }); }); compatTextStyle(toObj(option.axisPointer), 'label'); compatTextStyle(toObj(option.tooltip).axisPointer, 'label'); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Compatitable with 2.0 function get(opt, path) { path = path.split(','); var obj = opt; for (var i = 0; i < path.length; i++) { obj = obj && obj[path[i]]; if (obj == null) { break; } } return obj; } function set$1(opt, path, val, overwrite) { path = path.split(','); var obj = opt; var key; for (var i = 0; i < path.length - 1; i++) { key = path[i]; if (obj[key] == null) { obj[key] = {}; } obj = obj[key]; } if (overwrite || obj[path[i]] == null) { obj[path[i]] = val; } } function compatLayoutProperties(option) { each$1(LAYOUT_PROPERTIES, function (prop) { if (prop[0] in option && !(prop[1] in option)) { option[prop[1]] = option[prop[0]]; } }); } var LAYOUT_PROPERTIES = [ ['x', 'left'], ['y', 'top'], ['x2', 'right'], ['y2', 'bottom'] ]; var COMPATITABLE_COMPONENTS = [ 'grid', 'geo', 'parallel', 'legend', 'toolbox', 'title', 'visualMap', 'dataZoom', 'timeline' ]; var backwardCompat = function (option, isTheme) { compatStyle(option, isTheme); // Make sure series array for model initialization. option.series = normalizeToArray(option.series); each$1(option.series, function (seriesOpt) { if (!isObject$1(seriesOpt)) { return; } var seriesType = seriesOpt.type; if (seriesType === 'line') { if (seriesOpt.clipOverflow != null) { seriesOpt.clip = seriesOpt.clipOverflow; } } else if (seriesType === 'pie' || seriesType === 'gauge') { if (seriesOpt.clockWise != null) { seriesOpt.clockwise = seriesOpt.clockWise; } } else if (seriesType === 'gauge') { var pointerColor = get(seriesOpt, 'pointer.color'); pointerColor != null && set$1(seriesOpt, 'itemStyle.color', pointerColor); } compatLayoutProperties(seriesOpt); }); // dataRange has changed to visualMap if (option.dataRange) { option.visualMap = option.dataRange; } each$1(COMPATITABLE_COMPONENTS, function (componentName) { var options = option[componentName]; if (options) { if (!isArray(options)) { options = [options]; } each$1(options, function (option) { compatLayoutProperties(option); }); } }); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // (1) [Caution]: the logic is correct based on the premises: // data processing stage is blocked in stream. // See // (2) Only register once when import repeatly. // Should be executed after series filtered and before stack calculation. var dataStack = function (ecModel) { var stackInfoMap = createHashMap(); ecModel.eachSeries(function (seriesModel) { var stack = seriesModel.get('stack'); // Compatibal: when `stack` is set as '', do not stack. if (stack) { var stackInfoList = stackInfoMap.get(stack) || stackInfoMap.set(stack, []); var data = seriesModel.getData(); var stackInfo = { // Used for calculate axis extent automatically. stackResultDimension: data.getCalculationInfo('stackResultDimension'), stackedOverDimension: data.getCalculationInfo('stackedOverDimension'), stackedDimension: data.getCalculationInfo('stackedDimension'), stackedByDimension: data.getCalculationInfo('stackedByDimension'), isStackedByIndex: data.getCalculationInfo('isStackedByIndex'), data: data, seriesModel: seriesModel }; // If stacked on axis that do not support data stack. if (!stackInfo.stackedDimension || !(stackInfo.isStackedByIndex || stackInfo.stackedByDimension) ) { return; } stackInfoList.length && data.setCalculationInfo( 'stackedOnSeries', stackInfoList[stackInfoList.length - 1].seriesModel ); stackInfoList.push(stackInfo); } }); stackInfoMap.each(calculateStack); }; function calculateStack(stackInfoList) { each$1(stackInfoList, function (targetStackInfo, idxInStack) { var resultVal = []; var resultNaN = [NaN, NaN]; var dims = [targetStackInfo.stackResultDimension, targetStackInfo.stackedOverDimension]; var targetData = targetStackInfo.data; var isStackedByIndex = targetStackInfo.isStackedByIndex; // Should not write on raw data, because stack series model list changes // depending on legend selection. var newData = targetData.map(dims, function (v0, v1, dataIndex) { var sum = targetData.get(targetStackInfo.stackedDimension, dataIndex); // Consider `connectNulls` of line area, if value is NaN, stackedOver // should also be NaN, to draw a appropriate belt area. if (isNaN(sum)) { return resultNaN; } var byValue; var stackedDataRawIndex; if (isStackedByIndex) { stackedDataRawIndex = targetData.getRawIndex(dataIndex); } else { byValue = targetData.get(targetStackInfo.stackedByDimension, dataIndex); } // If stackOver is NaN, chart view will render point on value start. var stackedOver = NaN; for (var j = idxInStack - 1; j >= 0; j--) { var stackInfo = stackInfoList[j]; // Has been optimized by inverted indices on `stackedByDimension`. if (!isStackedByIndex) { stackedDataRawIndex = stackInfo.data.rawIndexOf(stackInfo.stackedByDimension, byValue); } if (stackedDataRawIndex >= 0) { var val = stackInfo.data.getByRawIndex(stackInfo.stackResultDimension, stackedDataRawIndex); // Considering positive stack, negative stack and empty data if ((sum >= 0 && val > 0) // Positive stack || (sum <= 0 && val < 0) // Negative stack ) { sum += val; stackedOver = val; break; } } } resultVal[0] = sum; resultVal[1] = stackedOver; return resultVal; }); targetData.hostModel.setData(newData); // Update for consequent calculation targetStackInfo.data = newData; }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // TODO // ??? refactor? check the outer usage of data provider. // merge with defaultDimValueGetter? /** * If normal array used, mutable chunk size is supported. * If typed array used, chunk size must be fixed. */ function DefaultDataProvider(source, dimSize) { if (!Source.isInstance(source)) { source = Source.seriesDataToSource(source); } this._source = source; var data = this._data = source.data; var sourceFormat = source.sourceFormat; // Typed array. TODO IE10+? if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { if (__DEV__) { if (dimSize == null) { throw new Error('Typed array data must specify dimension size'); } } this._offset = 0; this._dimSize = dimSize; this._data = data; } var methods = providerMethods[ sourceFormat === SOURCE_FORMAT_ARRAY_ROWS ? sourceFormat + '_' + source.seriesLayoutBy : sourceFormat ]; if (__DEV__) { assert$1(methods, 'Invalide sourceFormat: ' + sourceFormat); } extend(this, methods); } var providerProto = DefaultDataProvider.prototype; // If data is pure without style configuration providerProto.pure = false; // If data is persistent and will not be released after use. providerProto.persistent = true; // ???! FIXME legacy data provider do not has method getSource providerProto.getSource = function () { return this._source; }; var providerMethods = { 'arrayRows_column': { pure: true, count: function () { return Math.max(0, this._data.length - this._source.startIndex); }, getItem: function (idx) { return this._data[idx + this._source.startIndex]; }, appendData: appendDataSimply }, 'arrayRows_row': { pure: true, count: function () { var row = this._data[0]; return row ? Math.max(0, row.length - this._source.startIndex) : 0; }, getItem: function (idx) { idx += this._source.startIndex; var item = []; var data = this._data; for (var i = 0; i < data.length; i++) { var row = data[i]; item.push(row ? row[idx] : null); } return item; }, appendData: function () { throw new Error('Do not support appendData when set seriesLayoutBy: "row".'); } }, 'objectRows': { pure: true, count: countSimply, getItem: getItemSimply, appendData: appendDataSimply }, 'keyedColumns': { pure: true, count: function () { var dimName = this._source.dimensionsDefine[0].name; var col = this._data[dimName]; return col ? col.length : 0; }, getItem: function (idx) { var item = []; var dims = this._source.dimensionsDefine; for (var i = 0; i < dims.length; i++) { var col = this._data[dims[i].name]; item.push(col ? col[idx] : null); } return item; }, appendData: function (newData) { var data = this._data; each$1(newData, function (newCol, key) { var oldCol = data[key] || (data[key] = []); for (var i = 0; i < (newCol || []).length; i++) { oldCol.push(newCol[i]); } }); } }, 'original': { count: countSimply, getItem: getItemSimply, appendData: appendDataSimply }, 'typedArray': { persistent: false, pure: true, count: function () { return this._data ? (this._data.length / this._dimSize) : 0; }, getItem: function (idx, out) { idx = idx - this._offset; out = out || []; var offset = this._dimSize * idx; for (var i = 0; i < this._dimSize; i++) { out[i] = this._data[offset + i]; } return out; }, appendData: function (newData) { if (__DEV__) { assert$1( isTypedArray(newData), 'Added data must be TypedArray if data in initialization is TypedArray' ); } this._data = newData; }, // Clean self if data is already used. clean: function () { // PENDING this._offset += this.count(); this._data = null; } } }; function countSimply() { return this._data.length; } function getItemSimply(idx) { return this._data[idx]; } function appendDataSimply(newData) { for (var i = 0; i < newData.length; i++) { this._data.push(newData[i]); } } var rawValueGetters = { arrayRows: getRawValueSimply, objectRows: function (dataItem, dataIndex, dimIndex, dimName) { return dimIndex != null ? dataItem[dimName] : dataItem; }, keyedColumns: getRawValueSimply, original: function (dataItem, dataIndex, dimIndex, dimName) { // FIXME // In some case (markpoint in geo (geo-map.html)), dataItem // is {coord: [...]} var value = getDataItemValue(dataItem); return (dimIndex == null || !(value instanceof Array)) ? value : value[dimIndex]; }, typedArray: getRawValueSimply }; function getRawValueSimply(dataItem, dataIndex, dimIndex, dimName) { return dimIndex != null ? dataItem[dimIndex] : dataItem; } var defaultDimValueGetters = { arrayRows: getDimValueSimply, objectRows: function (dataItem, dimName, dataIndex, dimIndex) { return converDataValue(dataItem[dimName], this._dimensionInfos[dimName]); }, keyedColumns: getDimValueSimply, original: function (dataItem, dimName, dataIndex, dimIndex) { // Performance sensitive, do not use modelUtil.getDataItemValue. // If dataItem is an plain object with no value field, the var `value` // will be assigned with the object, but it will be tread correctly // in the `convertDataValue`. var value = dataItem && (dataItem.value == null ? dataItem : dataItem.value); // If any dataItem is like { value: 10 } if (!this._rawData.pure && isDataItemOption(dataItem)) { this.hasItemOption = true; } return converDataValue( (value instanceof Array) ? value[dimIndex] // If value is a single number or something else not array. : value, this._dimensionInfos[dimName] ); }, typedArray: function (dataItem, dimName, dataIndex, dimIndex) { return dataItem[dimIndex]; } }; function getDimValueSimply(dataItem, dimName, dataIndex, dimIndex) { return converDataValue(dataItem[dimIndex], this._dimensionInfos[dimName]); } /** * This helper method convert value in data. * @param {string|number|Date} value * @param {Object|string} [dimInfo] If string (like 'x'), dimType defaults 'number'. * If "dimInfo.ordinalParseAndSave", ordinal value can be parsed. */ function converDataValue(value, dimInfo) { // Performance sensitive. var dimType = dimInfo && dimInfo.type; if (dimType === 'ordinal') { // If given value is a category string var ordinalMeta = dimInfo && dimInfo.ordinalMeta; return ordinalMeta ? ordinalMeta.parseAndCollect(value) : value; } if (dimType === 'time' // spead up when using timestamp && typeof value !== 'number' && value != null && value !== '-' ) { value = +parseDate(value); } // dimType defaults 'number'. // If dimType is not ordinal and value is null or undefined or NaN or '-', // parse to NaN. return (value == null || value === '') ? NaN // If string (like '-'), using '+' parse to NaN // If object, also parse to NaN : +value; } // ??? FIXME can these logic be more neat: getRawValue, getRawDataItem, // Consider persistent. // Caution: why use raw value to display on label or tooltip? // A reason is to avoid format. For example time value we do not know // how to format is expected. More over, if stack is used, calculated // value may be 0.91000000001, which have brings trouble to display. // TODO: consider how to treat null/undefined/NaN when display? /** * @param {module:echarts/data/List} data * @param {number} dataIndex * @param {string|number} [dim] dimName or dimIndex * @return {Array.|string|number} can be null/undefined. */ function retrieveRawValue(data, dataIndex, dim) { if (!data) { return; } // Consider data may be not persistent. var dataItem = data.getRawDataItem(dataIndex); if (dataItem == null) { return; } var sourceFormat = data.getProvider().getSource().sourceFormat; var dimName; var dimIndex; var dimInfo = data.getDimensionInfo(dim); if (dimInfo) { dimName = dimInfo.name; dimIndex = dimInfo.index; } return rawValueGetters[sourceFormat](dataItem, dataIndex, dimIndex, dimName); } /** * Compatible with some cases (in pie, map) like: * data: [{name: 'xx', value: 5, selected: true}, ...] * where only sourceFormat is 'original' and 'objectRows' supported. * * ??? TODO * Supported detail options in data item when using 'arrayRows'. * * @param {module:echarts/data/List} data * @param {number} dataIndex * @param {string} attr like 'selected' */ function retrieveRawAttr(data, dataIndex, attr) { if (!data) { return; } var sourceFormat = data.getProvider().getSource().sourceFormat; if (sourceFormat !== SOURCE_FORMAT_ORIGINAL && sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS ) { return; } var dataItem = data.getRawDataItem(dataIndex); if (sourceFormat === SOURCE_FORMAT_ORIGINAL && !isObject$1(dataItem)) { dataItem = null; } if (dataItem) { return dataItem[attr]; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var DIMENSION_LABEL_REG = /\{@(.+?)\}/g; // PENDING A little ugly var dataFormatMixin = { /** * Get params for formatter * @param {number} dataIndex * @param {string} [dataType] * @return {Object} */ getDataParams: function (dataIndex, dataType) { var data = this.getData(dataType); var rawValue = this.getRawValue(dataIndex, dataType); var rawDataIndex = data.getRawIndex(dataIndex); var name = data.getName(dataIndex); var itemOpt = data.getRawDataItem(dataIndex); var color = data.getItemVisual(dataIndex, 'color'); var borderColor = data.getItemVisual(dataIndex, 'borderColor'); var tooltipModel = this.ecModel.getComponent('tooltip'); var renderModeOption = tooltipModel && tooltipModel.get('renderMode'); var renderMode = getTooltipRenderMode(renderModeOption); var mainType = this.mainType; var isSeries = mainType === 'series'; var userOutput = data.userOutput; return { componentType: mainType, componentSubType: this.subType, componentIndex: this.componentIndex, seriesType: isSeries ? this.subType : null, seriesIndex: this.seriesIndex, seriesId: isSeries ? this.id : null, seriesName: isSeries ? this.name : null, name: name, dataIndex: rawDataIndex, data: itemOpt, dataType: dataType, value: rawValue, color: color, borderColor: borderColor, dimensionNames: userOutput ? userOutput.dimensionNames : null, encode: userOutput ? userOutput.encode : null, marker: getTooltipMarker({ color: color, renderMode: renderMode }), // Param name list for mapping `a`, `b`, `c`, `d`, `e` $vars: ['seriesName', 'name', 'value'] }; }, /** * Format label * @param {number} dataIndex * @param {string} [status='normal'] 'normal' or 'emphasis' * @param {string} [dataType] * @param {number} [dimIndex] Only used in some chart that * use formatter in different dimensions, like radar. * @param {string} [labelProp='label'] * @return {string} If not formatter, return null/undefined */ getFormattedLabel: function (dataIndex, status, dataType, dimIndex, labelProp) { status = status || 'normal'; var data = this.getData(dataType); var itemModel = data.getItemModel(dataIndex); var params = this.getDataParams(dataIndex, dataType); if (dimIndex != null && (params.value instanceof Array)) { params.value = params.value[dimIndex]; } var formatter = itemModel.get( status === 'normal' ? [labelProp || 'label', 'formatter'] : [status, labelProp || 'label', 'formatter'] ); if (typeof formatter === 'function') { params.status = status; params.dimensionIndex = dimIndex; return formatter(params); } else if (typeof formatter === 'string') { var str = formatTpl(formatter, params); // Support 'aaa{@[3]}bbb{@product}ccc'. // Do not support '}' in dim name util have to. return str.replace(DIMENSION_LABEL_REG, function (origin, dim) { var len = dim.length; if (dim.charAt(0) === '[' && dim.charAt(len - 1) === ']') { dim = +dim.slice(1, len - 1); // Also: '[]' => 0 } return retrieveRawValue(data, dataIndex, dim); }); } }, /** * Get raw value in option * @param {number} idx * @param {string} [dataType] * @return {Array|number|string} */ getRawValue: function (idx, dataType) { return retrieveRawValue(this.getData(dataType), idx); }, /** * Should be implemented. * @param {number} dataIndex * @param {boolean} [multipleSeries=false] * @param {number} [dataType] * @return {string} tooltip string */ formatTooltip: function () { // Empty function } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {Object} define * @return See the return of `createTask`. */ function createTask(define) { return new Task(define); } /** * @constructor * @param {Object} define * @param {Function} define.reset Custom reset * @param {Function} [define.plan] Returns 'reset' indicate reset immediately. * @param {Function} [define.count] count is used to determin data task. * @param {Function} [define.onDirty] count is used to determin data task. */ function Task(define) { define = define || {}; this._reset = define.reset; this._plan = define.plan; this._count = define.count; this._onDirty = define.onDirty; this._dirty = true; // Context must be specified implicitly, to // avoid miss update context when model changed. this.context; } var taskProto = Task.prototype; /** * @param {Object} performArgs * @param {number} [performArgs.step] Specified step. * @param {number} [performArgs.skip] Skip customer perform call. * @param {number} [performArgs.modBy] Sampling window size. * @param {number} [performArgs.modDataCount] Sampling count. */ taskProto.perform = function (performArgs) { var upTask = this._upstream; var skip = performArgs && performArgs.skip; // TODO some refactor. // Pull data. Must pull data each time, because context.data // may be updated by Series.setData. if (this._dirty && upTask) { var context = this.context; context.data = context.outputData = upTask.context.outputData; } if (this.__pipeline) { this.__pipeline.currentTask = this; } var planResult; if (this._plan && !skip) { planResult = this._plan(this.context); } // Support sharding by mod, which changes the render sequence and makes the rendered graphic // elements uniformed distributed when progress, especially when moving or zooming. var lastModBy = normalizeModBy(this._modBy); var lastModDataCount = this._modDataCount || 0; var modBy = normalizeModBy(performArgs && performArgs.modBy); var modDataCount = performArgs && performArgs.modDataCount || 0; if (lastModBy !== modBy || lastModDataCount !== modDataCount) { planResult = 'reset'; } function normalizeModBy(val) { !(val >= 1) && (val = 1); // jshint ignore:line return val; } var forceFirstProgress; if (this._dirty || planResult === 'reset') { this._dirty = false; forceFirstProgress = reset(this, skip); } this._modBy = modBy; this._modDataCount = modDataCount; var step = performArgs && performArgs.step; if (upTask) { if (__DEV__) { assert$1(upTask._outputDueEnd != null); } this._dueEnd = upTask._outputDueEnd; } // DataTask or overallTask else { if (__DEV__) { assert$1(!this._progress || this._count); } this._dueEnd = this._count ? this._count(this.context) : Infinity; } // Note: Stubs, that its host overall task let it has progress, has progress. // If no progress, pass index from upstream to downstream each time plan called. if (this._progress) { var start = this._dueIndex; var end = Math.min( step != null ? this._dueIndex + step : Infinity, this._dueEnd ); if (!skip && (forceFirstProgress || start < end)) { var progress = this._progress; if (isArray(progress)) { for (var i = 0; i < progress.length; i++) { doProgress(this, progress[i], start, end, modBy, modDataCount); } } else { doProgress(this, progress, start, end, modBy, modDataCount); } } this._dueIndex = end; // If no `outputDueEnd`, assume that output data and // input data is the same, so use `dueIndex` as `outputDueEnd`. var outputDueEnd = this._settedOutputEnd != null ? this._settedOutputEnd : end; if (__DEV__) { // ??? Can not rollback. assert$1(outputDueEnd >= this._outputDueEnd); } this._outputDueEnd = outputDueEnd; } else { // (1) Some overall task has no progress. // (2) Stubs, that its host overall task do not let it has progress, has no progress. // This should always be performed so it can be passed to downstream. this._dueIndex = this._outputDueEnd = this._settedOutputEnd != null ? this._settedOutputEnd : this._dueEnd; } return this.unfinished(); }; var iterator = (function () { var end; var current; var modBy; var modDataCount; var winCount; var it = { reset: function (s, e, sStep, sCount) { current = s; end = e; modBy = sStep; modDataCount = sCount; winCount = Math.ceil(modDataCount / modBy); it.next = (modBy > 1 && modDataCount > 0) ? modNext : sequentialNext; } }; return it; function sequentialNext() { return current < end ? current++ : null; } function modNext() { var dataIndex = (current % winCount) * modBy + Math.ceil(current / winCount); var result = current >= end ? null : dataIndex < modDataCount ? dataIndex // If modDataCount is smaller than data.count() (consider `appendData` case), // Use normal linear rendering mode. : current; current++; return result; } })(); taskProto.dirty = function () { this._dirty = true; this._onDirty && this._onDirty(this.context); }; function doProgress(taskIns, progress, start, end, modBy, modDataCount) { iterator.reset(start, end, modBy, modDataCount); taskIns._callingProgress = progress; taskIns._callingProgress({ start: start, end: end, count: end - start, next: iterator.next }, taskIns.context); } function reset(taskIns, skip) { taskIns._dueIndex = taskIns._outputDueEnd = taskIns._dueEnd = 0; taskIns._settedOutputEnd = null; var progress; var forceFirstProgress; if (!skip && taskIns._reset) { progress = taskIns._reset(taskIns.context); if (progress && progress.progress) { forceFirstProgress = progress.forceFirstProgress; progress = progress.progress; } // To simplify no progress checking, array must has item. if (isArray(progress) && !progress.length) { progress = null; } } taskIns._progress = progress; taskIns._modBy = taskIns._modDataCount = null; var downstream = taskIns._downstream; downstream && downstream.dirty(); return forceFirstProgress; } /** * @return {boolean} */ taskProto.unfinished = function () { return this._progress && this._dueIndex < this._dueEnd; }; /** * @param {Object} downTask The downstream task. * @return {Object} The downstream task. */ taskProto.pipe = function (downTask) { if (__DEV__) { assert$1(downTask && !downTask._disposed && downTask !== this); } // If already downstream, do not dirty downTask. if (this._downstream !== downTask || this._dirty) { this._downstream = downTask; downTask._upstream = this; downTask.dirty(); } }; taskProto.dispose = function () { if (this._disposed) { return; } this._upstream && (this._upstream._downstream = null); this._downstream && (this._downstream._upstream = null); this._dirty = false; this._disposed = true; }; taskProto.getUpstream = function () { return this._upstream; }; taskProto.getDownstream = function () { return this._downstream; }; taskProto.setOutputEnd = function (end) { // This only happend in dataTask, dataZoom, map, currently. // where dataZoom do not set end each time, but only set // when reset. So we should record the setted end, in case // that the stub of dataZoom perform again and earse the // setted end by upstream. this._outputDueEnd = this._settedOutputEnd = end; }; /////////////////////////////////////////////////////////// // For stream debug (Should be commented out after used!) // Usage: printTask(this, 'begin'); // Usage: printTask(this, null, {someExtraProp}); // function printTask(task, prefix, extra) { // window.ecTaskUID == null && (window.ecTaskUID = 0); // task.uidDebug == null && (task.uidDebug = `task_${window.ecTaskUID++}`); // task.agent && task.agent.uidDebug == null && (task.agent.uidDebug = `task_${window.ecTaskUID++}`); // var props = []; // if (task.__pipeline) { // var val = `${task.__idxInPipeline}/${task.__pipeline.tail.__idxInPipeline} ${task.agent ? '(stub)' : ''}`; // props.push({text: 'idx', value: val}); // } else { // var stubCount = 0; // task.agentStubMap.each(() => stubCount++); // props.push({text: 'idx', value: `overall (stubs: ${stubCount})`}); // } // props.push({text: 'uid', value: task.uidDebug}); // if (task.__pipeline) { // props.push({text: 'pid', value: task.__pipeline.id}); // task.agent && props.push( // {text: 'stubFor', value: task.agent.uidDebug} // ); // } // props.push( // {text: 'dirty', value: task._dirty}, // {text: 'dueIndex', value: task._dueIndex}, // {text: 'dueEnd', value: task._dueEnd}, // {text: 'outputDueEnd', value: task._outputDueEnd} // ); // if (extra) { // Object.keys(extra).forEach(key => { // props.push({text: key, value: extra[key]}); // }); // } // var args = ['color: blue']; // var msg = `%c[${prefix || 'T'}] %c` + props.map(item => ( // args.push('color: black', 'color: red'), // `${item.text}: %c${item.value}` // )).join('%c, '); // console.log.apply(console, [msg].concat(args)); // // console.log(this); // } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$4 = makeInner(); var SeriesModel = ComponentModel.extend({ type: 'series.__base__', /** * @readOnly */ seriesIndex: 0, // coodinateSystem will be injected in the echarts/CoordinateSystem coordinateSystem: null, /** * @type {Object} * @protected */ defaultOption: null, /** * legend visual provider to the legend component * @type {Object} */ // PENDING legendVisualProvider: null, /** * Access path of color for visual */ visualColorAccessPath: 'itemStyle.color', /** * Access path of borderColor for visual */ visualBorderColorAccessPath: 'itemStyle.borderColor', /** * Support merge layout params. * Only support 'box' now (left/right/top/bottom/width/height). * @type {string|Object} Object can be {ignoreSize: true} * @readOnly */ layoutMode: null, init: function (option, parentModel, ecModel, extraOpt) { /** * @type {number} * @readOnly */ this.seriesIndex = this.componentIndex; this.dataTask = createTask({ count: dataTaskCount, reset: dataTaskReset }); this.dataTask.context = {model: this}; this.mergeDefaultAndTheme(option, ecModel); prepareSource(this); var data = this.getInitialData(option, ecModel); wrapData(data, this); this.dataTask.context.data = data; if (__DEV__) { assert$1(data, 'getInitialData returned invalid data.'); } /** * @type {module:echarts/data/List|module:echarts/data/Tree|module:echarts/data/Graph} * @private */ inner$4(this).dataBeforeProcessed = data; // If we reverse the order (make data firstly, and then make // dataBeforeProcessed by cloneShallow), cloneShallow will // cause data.graph.data !== data when using // module:echarts/data/Graph or module:echarts/data/Tree. // See module:echarts/data/helper/linkList // Theoretically, it is unreasonable to call `seriesModel.getData()` in the model // init or merge stage, because the data can be restored. So we do not `restoreData` // and `setData` here, which forbids calling `seriesModel.getData()` in this stage. // Call `seriesModel.getRawData()` instead. // this.restoreData(); autoSeriesName(this); }, /** * Util for merge default and theme to option * @param {Object} option * @param {module:echarts/model/Global} ecModel */ mergeDefaultAndTheme: function (option, ecModel) { var layoutMode = this.layoutMode; var inputPositionParams = layoutMode ? getLayoutParams(option) : {}; // Backward compat: using subType on theme. // But if name duplicate between series subType // (for example: parallel) add component mainType, // add suffix 'Series'. var themeSubType = this.subType; if (ComponentModel.hasClass(themeSubType)) { themeSubType += 'Series'; } merge( option, ecModel.getTheme().get(this.subType) ); merge(option, this.getDefaultOption()); // Default label emphasis `show` defaultEmphasis(option, 'label', ['show']); this.fillDataTextStyle(option.data); if (layoutMode) { mergeLayoutParam(option, inputPositionParams, layoutMode); } }, mergeOption: function (newSeriesOption, ecModel) { // this.settingTask.dirty(); newSeriesOption = merge(this.option, newSeriesOption, true); this.fillDataTextStyle(newSeriesOption.data); var layoutMode = this.layoutMode; if (layoutMode) { mergeLayoutParam(this.option, newSeriesOption, layoutMode); } prepareSource(this); var data = this.getInitialData(newSeriesOption, ecModel); wrapData(data, this); this.dataTask.dirty(); this.dataTask.context.data = data; inner$4(this).dataBeforeProcessed = data; autoSeriesName(this); }, fillDataTextStyle: function (data) { // Default data label emphasis `show` // FIXME Tree structure data ? // FIXME Performance ? if (data && !isTypedArray(data)) { var props = ['show']; for (var i = 0; i < data.length; i++) { if (data[i] && data[i].label) { defaultEmphasis(data[i], 'label', props); } } } }, /** * Init a data structure from data related option in series * Must be overwritten */ getInitialData: function () {}, /** * Append data to list * @param {Object} params * @param {Array|TypedArray} params.data */ appendData: function (params) { // FIXME ??? // (1) If data from dataset, forbidden append. // (2) support append data of dataset. var data = this.getRawData(); data.appendData(params.data); }, /** * Consider some method like `filter`, `map` need make new data, * We should make sure that `seriesModel.getData()` get correct * data in the stream procedure. So we fetch data from upstream * each time `task.perform` called. * @param {string} [dataType] * @return {module:echarts/data/List} */ getData: function (dataType) { var task = getCurrentTask(this); if (task) { var data = task.context.data; return dataType == null ? data : data.getLinkedData(dataType); } else { // When series is not alive (that may happen when click toolbox // restore or setOption with not merge mode), series data may // be still need to judge animation or something when graphic // elements want to know whether fade out. return inner$4(this).data; } }, /** * @param {module:echarts/data/List} data */ setData: function (data) { var task = getCurrentTask(this); if (task) { var context = task.context; // Consider case: filter, data sample. if (context.data !== data && task.modifyOutputEnd) { task.setOutputEnd(data.count()); } context.outputData = data; // Caution: setData should update context.data, // Because getData may be called multiply in a // single stage and expect to get the data just // set. (For example, AxisProxy, x y both call // getData and setDate sequentially). // So the context.data should be fetched from // upstream each time when a stage starts to be // performed. if (task !== this.dataTask) { context.data = data; } } inner$4(this).data = data; }, /** * @see {module:echarts/data/helper/sourceHelper#getSource} * @return {module:echarts/data/Source} source */ getSource: function () { return getSource(this); }, /** * Get data before processed * @return {module:echarts/data/List} */ getRawData: function () { return inner$4(this).dataBeforeProcessed; }, /** * Get base axis if has coordinate system and has axis. * By default use coordSys.getBaseAxis(); * Can be overrided for some chart. * @return {type} description */ getBaseAxis: function () { var coordSys = this.coordinateSystem; return coordSys && coordSys.getBaseAxis && coordSys.getBaseAxis(); }, // FIXME /** * Default tooltip formatter * * @param {number} dataIndex * @param {boolean} [multipleSeries=false] * @param {number} [dataType] * @param {string} [renderMode='html'] valid values: 'html' and 'richText'. * 'html' is used for rendering tooltip in extra DOM form, and the result * string is used as DOM HTML content. * 'richText' is used for rendering tooltip in rich text form, for those where * DOM operation is not supported. * @return {Object} formatted tooltip with `html` and `markers` */ formatTooltip: function (dataIndex, multipleSeries, dataType, renderMode) { var series = this; renderMode = renderMode || 'html'; var newLine = renderMode === 'html' ? '
' : '\n'; var isRichText = renderMode === 'richText'; var markers = {}; var markerId = 0; function formatArrayValue(value) { // ??? TODO refactor these logic. // check: category-no-encode-has-axis-data in dataset.html var vertially = reduce(value, function (vertially, val, idx) { var dimItem = data.getDimensionInfo(idx); return vertially |= dimItem && dimItem.tooltip !== false && dimItem.displayName != null; }, 0); var result = []; tooltipDims.length ? each$1(tooltipDims, function (dim) { setEachItem(retrieveRawValue(data, dataIndex, dim), dim); }) // By default, all dims is used on tooltip. : each$1(value, setEachItem); function setEachItem(val, dim) { var dimInfo = data.getDimensionInfo(dim); // If `dimInfo.tooltip` is not set, show tooltip. if (!dimInfo || dimInfo.otherDims.tooltip === false) { return; } var dimType = dimInfo.type; var markName = 'sub' + series.seriesIndex + 'at' + markerId; var dimHead = getTooltipMarker({ color: color, type: 'subItem', renderMode: renderMode, markerId: markName }); var dimHeadStr = typeof dimHead === 'string' ? dimHead : dimHead.content; var valStr = (vertially ? dimHeadStr + encodeHTML(dimInfo.displayName || '-') + ': ' : '' ) // FIXME should not format time for raw data? + encodeHTML(dimType === 'ordinal' ? val + '' : dimType === 'time' ? (multipleSeries ? '' : formatTime('yyyy/MM/dd hh:mm:ss', val)) : addCommas(val) ); valStr && result.push(valStr); if (isRichText) { markers[markName] = color; ++markerId; } } var newLine = vertially ? (isRichText ? '\n' : '
') : ''; var content = newLine + result.join(newLine || ', '); return { renderMode: renderMode, content: content, style: markers }; } function formatSingleValue(val) { // return encodeHTML(addCommas(val)); return { renderMode: renderMode, content: encodeHTML(addCommas(val)), style: markers }; } var data = this.getData(); var tooltipDims = data.mapDimension('defaultedTooltip', true); var tooltipDimLen = tooltipDims.length; var value = this.getRawValue(dataIndex); var isValueArr = isArray(value); var color = data.getItemVisual(dataIndex, 'color'); if (isObject$1(color) && color.colorStops) { color = (color.colorStops[0] || {}).color; } color = color || 'transparent'; // Complicated rule for pretty tooltip. var formattedValue = (tooltipDimLen > 1 || (isValueArr && !tooltipDimLen)) ? formatArrayValue(value) : tooltipDimLen ? formatSingleValue(retrieveRawValue(data, dataIndex, tooltipDims[0])) : formatSingleValue(isValueArr ? value[0] : value); var content = formattedValue.content; var markName = series.seriesIndex + 'at' + markerId; var colorEl = getTooltipMarker({ color: color, type: 'item', renderMode: renderMode, markerId: markName }); markers[markName] = color; ++markerId; var name = data.getName(dataIndex); var seriesName = this.name; if (!isNameSpecified(this)) { seriesName = ''; } seriesName = seriesName ? encodeHTML(seriesName) + (!multipleSeries ? newLine : ': ') : ''; var colorStr = typeof colorEl === 'string' ? colorEl : colorEl.content; var html = !multipleSeries ? seriesName + colorStr + (name ? encodeHTML(name) + ': ' + content : content ) : colorStr + seriesName + content; return { html: html, markers: markers }; }, /** * @return {boolean} */ isAnimationEnabled: function () { if (env$1.node) { return false; } var animationEnabled = this.getShallow('animation'); if (animationEnabled) { if (this.getData().count() > this.getShallow('animationThreshold')) { animationEnabled = false; } } return animationEnabled; }, restoreData: function () { this.dataTask.dirty(); }, getColorFromPalette: function (name, scope, requestColorNum) { var ecModel = this.ecModel; // PENDING var color = colorPaletteMixin.getColorFromPalette.call(this, name, scope, requestColorNum); if (!color) { color = ecModel.getColorFromPalette(name, scope, requestColorNum); } return color; }, /** * Use `data.mapDimension(coordDim, true)` instead. * @deprecated */ coordDimToDataDim: function (coordDim) { return this.getRawData().mapDimension(coordDim, true); }, /** * Get progressive rendering count each step * @return {number} */ getProgressive: function () { return this.get('progressive'); }, /** * Get progressive rendering count each step * @return {number} */ getProgressiveThreshold: function () { return this.get('progressiveThreshold'); }, /** * Get data indices for show tooltip content. See tooltip. * @abstract * @param {Array.|string} dim * @param {Array.} value * @param {module:echarts/coord/single/SingleAxis} baseAxis * @return {Object} {dataIndices, nestestValue}. */ getAxisTooltipData: null, /** * See tooltip. * @abstract * @param {number} dataIndex * @return {Array.} Point of tooltip. null/undefined can be returned. */ getTooltipPosition: null, /** * @see {module:echarts/stream/Scheduler} */ pipeTask: null, /** * Convinient for override in extended class. * @protected * @type {Function} */ preventIncremental: null, /** * @public * @readOnly * @type {Object} */ pipelineContext: null }); mixin(SeriesModel, dataFormatMixin); mixin(SeriesModel, colorPaletteMixin); /** * MUST be called after `prepareSource` called * Here we need to make auto series, especially for auto legend. But we * do not modify series.name in option to avoid side effects. */ function autoSeriesName(seriesModel) { // User specified name has higher priority, otherwise it may cause // series can not be queried unexpectedly. var name = seriesModel.name; if (!isNameSpecified(seriesModel)) { seriesModel.name = getSeriesAutoName(seriesModel) || name; } } function getSeriesAutoName(seriesModel) { var data = seriesModel.getRawData(); var dataDims = data.mapDimension('seriesName', true); var nameArr = []; each$1(dataDims, function (dataDim) { var dimInfo = data.getDimensionInfo(dataDim); dimInfo.displayName && nameArr.push(dimInfo.displayName); }); return nameArr.join(' '); } function dataTaskCount(context) { return context.model.getRawData().count(); } function dataTaskReset(context) { var seriesModel = context.model; seriesModel.setData(seriesModel.getRawData().cloneShallow()); return dataTaskProgress; } function dataTaskProgress(param, context) { // Avoid repead cloneShallow when data just created in reset. if (context.outputData && param.end > context.outputData.count()) { context.model.getRawData().cloneShallow(context.outputData); } } // TODO refactor function wrapData(data, seriesModel) { each$1(data.CHANGABLE_METHODS, function (methodName) { data.wrapMethod(methodName, curry(onDataSelfChange, seriesModel)); }); } function onDataSelfChange(seriesModel) { var task = getCurrentTask(seriesModel); if (task) { // Consider case: filter, selectRange task.setOutputEnd(this.count()); } } function getCurrentTask(seriesModel) { var scheduler = (seriesModel.ecModel || {}).scheduler; var pipeline = scheduler && scheduler.getPipeline(seriesModel.uid); if (pipeline) { // When pipline finished, the currrentTask keep the last // task (renderTask). var task = pipeline.currentTask; if (task) { var agentStubMap = task.agentStubMap; if (agentStubMap) { task = agentStubMap.get(seriesModel.uid); } } return task; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var Component$1 = function () { /** * @type {module:zrender/container/Group} * @readOnly */ this.group = new Group(); /** * @type {string} * @readOnly */ this.uid = getUID('viewComponent'); }; Component$1.prototype = { constructor: Component$1, init: function (ecModel, api) {}, render: function (componentModel, ecModel, api, payload) {}, dispose: function () {}, /** * @param {string} eventType * @param {Object} query * @param {module:zrender/Element} targetEl * @param {Object} packedEvent * @return {boolen} Pass only when return `true`. */ filterForExposedEvent: null }; var componentProto = Component$1.prototype; componentProto.updateView = componentProto.updateLayout = componentProto.updateVisual = function (seriesModel, ecModel, api, payload) { // Do nothing; }; // Enable Component.extend. enableClassExtend(Component$1); // Enable capability of registerClass, getClass, hasClass, registerSubTypeDefaulter and so on. enableClassManagement(Component$1, {registerWhenExtend: true}); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @return {string} If large mode changed, return string 'reset'; */ var createRenderPlanner = function () { var inner = makeInner(); return function (seriesModel) { var fields = inner(seriesModel); var pipelineContext = seriesModel.pipelineContext; var originalLarge = fields.large; var originalProgressive = fields.progressiveRender; // FIXME: if the planner works on a filtered series, `pipelineContext` does not // exists. See #11611 . Probably we need to modify this structure, see the comment // on `performRawSeries` in `Schedular.js`. var large = fields.large = pipelineContext && pipelineContext.large; var progressive = fields.progressiveRender = pipelineContext && pipelineContext.progressiveRender; return !!((originalLarge ^ large) || (originalProgressive ^ progressive)) && 'reset'; }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$5 = makeInner(); var renderPlanner = createRenderPlanner(); function Chart() { /** * @type {module:zrender/container/Group} * @readOnly */ this.group = new Group(); /** * @type {string} * @readOnly */ this.uid = getUID('viewChart'); this.renderTask = createTask({ plan: renderTaskPlan, reset: renderTaskReset }); this.renderTask.context = {view: this}; } Chart.prototype = { type: 'chart', /** * Init the chart. * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ init: function (ecModel, api) {}, /** * Render the chart. * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ render: function (seriesModel, ecModel, api, payload) {}, /** * Highlight series or specified data item. * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ highlight: function (seriesModel, ecModel, api, payload) { toggleHighlight(seriesModel.getData(), payload, 'emphasis'); }, /** * Downplay series or specified data item. * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ downplay: function (seriesModel, ecModel, api, payload) { toggleHighlight(seriesModel.getData(), payload, 'normal'); }, /** * Remove self. * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ remove: function (ecModel, api) { this.group.removeAll(); }, /** * Dispose self. * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ dispose: function () {}, /** * Rendering preparation in progressive mode. * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ incrementalPrepareRender: null, /** * Render in progressive mode. * @param {Object} params See taskParams in `stream/task.js` * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ incrementalRender: null, /** * Update transform directly. * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload * @return {Object} {update: true} */ updateTransform: null, /** * The view contains the given point. * @interface * @param {Array.} point * @return {boolean} */ // containPoint: function () {} /** * @param {string} eventType * @param {Object} query * @param {module:zrender/Element} targetEl * @param {Object} packedEvent * @return {boolen} Pass only when return `true`. */ filterForExposedEvent: null }; var chartProto = Chart.prototype; chartProto.updateView = chartProto.updateLayout = chartProto.updateVisual = function (seriesModel, ecModel, api, payload) { this.render(seriesModel, ecModel, api, payload); }; /** * Set state of single element * @param {module:zrender/Element} el * @param {string} state 'normal'|'emphasis' * @param {number} highlightDigit */ function elSetState(el, state, highlightDigit) { if (el) { el.trigger(state, highlightDigit); if (el.isGroup // Simple optimize. && !isHighDownDispatcher(el) ) { for (var i = 0, len = el.childCount(); i < len; i++) { elSetState(el.childAt(i), state, highlightDigit); } } } } /** * @param {module:echarts/data/List} data * @param {Object} payload * @param {string} state 'normal'|'emphasis' */ function toggleHighlight(data, payload, state) { var dataIndex = queryDataIndex(data, payload); var highlightDigit = (payload && payload.highlightKey != null) ? getHighlightDigit(payload.highlightKey) : null; if (dataIndex != null) { each$1(normalizeToArray(dataIndex), function (dataIdx) { elSetState(data.getItemGraphicEl(dataIdx), state, highlightDigit); }); } else { data.eachItemGraphicEl(function (el) { elSetState(el, state, highlightDigit); }); } } // Enable Chart.extend. enableClassExtend(Chart, ['dispose']); // Add capability of registerClass, getClass, hasClass, registerSubTypeDefaulter and so on. enableClassManagement(Chart, {registerWhenExtend: true}); Chart.markUpdateMethod = function (payload, methodName) { inner$5(payload).updateMethod = methodName; }; function renderTaskPlan(context) { return renderPlanner(context.model); } function renderTaskReset(context) { var seriesModel = context.model; var ecModel = context.ecModel; var api = context.api; var payload = context.payload; // ???! remove updateView updateVisual var progressiveRender = seriesModel.pipelineContext.progressiveRender; var view = context.view; var updateMethod = payload && inner$5(payload).updateMethod; var methodName = progressiveRender ? 'incrementalPrepareRender' : (updateMethod && view[updateMethod]) ? updateMethod // `appendData` is also supported when data amount // is less than progressive threshold. : 'render'; if (methodName !== 'render') { view[methodName](seriesModel, ecModel, api, payload); } return progressMethodMap[methodName]; } var progressMethodMap = { incrementalPrepareRender: { progress: function (params, context) { context.view.incrementalRender( params, context.model, context.ecModel, context.api, context.payload ); } }, render: { // Put view.render in `progress` to support appendData. But in this case // view.render should not be called in reset, otherwise it will be called // twise. Use `forceFirstProgress` to make sure that view.render is called // in any cases. forceFirstProgress: true, progress: function (params, context) { context.view.render( context.model, context.ecModel, context.api, context.payload ); } } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var ORIGIN_METHOD = '\0__throttleOriginMethod'; var RATE = '\0__throttleRate'; var THROTTLE_TYPE = '\0__throttleType'; /** * @public * @param {(Function)} fn * @param {number} [delay=0] Unit: ms. * @param {boolean} [debounce=false] * true: If call interval less than `delay`, only the last call works. * false: If call interval less than `delay, call works on fixed rate. * @return {(Function)} throttled fn. */ function throttle(fn, delay, debounce) { var currCall; var lastCall = 0; var lastExec = 0; var timer = null; var diff; var scope; var args; var debounceNextCall; delay = delay || 0; function exec() { lastExec = (new Date()).getTime(); timer = null; fn.apply(scope, args || []); } var cb = function () { currCall = (new Date()).getTime(); scope = this; args = arguments; var thisDelay = debounceNextCall || delay; var thisDebounce = debounceNextCall || debounce; debounceNextCall = null; diff = currCall - (thisDebounce ? lastCall : lastExec) - thisDelay; clearTimeout(timer); // Here we should make sure that: the `exec` SHOULD NOT be called later // than a new call of `cb`, that is, preserving the command order. Consider // calculating "scale rate" when roaming as an example. When a call of `cb` // happens, either the `exec` is called dierectly, or the call is delayed. // But the delayed call should never be later than next call of `cb`. Under // this assurance, we can simply update view state each time `dispatchAction` // triggered by user roaming, but not need to add extra code to avoid the // state being "rolled-back". if (thisDebounce) { timer = setTimeout(exec, thisDelay); } else { if (diff >= 0) { exec(); } else { timer = setTimeout(exec, -diff); } } lastCall = currCall; }; /** * Clear throttle. * @public */ cb.clear = function () { if (timer) { clearTimeout(timer); timer = null; } }; /** * Enable debounce once. */ cb.debounceNextCall = function (debounceDelay) { debounceNextCall = debounceDelay; }; return cb; } /** * Create throttle method or update throttle rate. * * @example * ComponentView.prototype.render = function () { * ... * throttle.createOrUpdate( * this, * '_dispatchAction', * this.model.get('throttle'), * 'fixRate' * ); * }; * ComponentView.prototype.remove = function () { * throttle.clear(this, '_dispatchAction'); * }; * ComponentView.prototype.dispose = function () { * throttle.clear(this, '_dispatchAction'); * }; * * @public * @param {Object} obj * @param {string} fnAttr * @param {number} [rate] * @param {string} [throttleType='fixRate'] 'fixRate' or 'debounce' * @return {Function} throttled function. */ function createOrUpdate(obj, fnAttr, rate, throttleType) { var fn = obj[fnAttr]; if (!fn) { return; } var originFn = fn[ORIGIN_METHOD] || fn; var lastThrottleType = fn[THROTTLE_TYPE]; var lastRate = fn[RATE]; if (lastRate !== rate || lastThrottleType !== throttleType) { if (rate == null || !throttleType) { return (obj[fnAttr] = originFn); } fn = obj[fnAttr] = throttle( originFn, rate, throttleType === 'debounce' ); fn[ORIGIN_METHOD] = originFn; fn[THROTTLE_TYPE] = throttleType; fn[RATE] = rate; } return fn; } /** * Clear throttle. Example see throttle.createOrUpdate. * * @public * @param {Object} obj * @param {string} fnAttr */ function clear(obj, fnAttr) { var fn = obj[fnAttr]; if (fn && fn[ORIGIN_METHOD]) { obj[fnAttr] = fn[ORIGIN_METHOD]; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var seriesColor = { createOnAllSeries: true, performRawSeries: true, reset: function (seriesModel, ecModel) { var data = seriesModel.getData(); var colorAccessPath = (seriesModel.visualColorAccessPath || 'itemStyle.color').split('.'); // Set in itemStyle var color = seriesModel.get(colorAccessPath); var colorCallback = (isFunction$1(color) && !(color instanceof Gradient)) ? color : null; // Default color if (!color || colorCallback) { color = seriesModel.getColorFromPalette( // TODO series count changed. seriesModel.name, null, ecModel.getSeriesCount() ); } data.setVisual('color', color); var borderColorAccessPath = (seriesModel.visualBorderColorAccessPath || 'itemStyle.borderColor').split('.'); var borderColor = seriesModel.get(borderColorAccessPath); data.setVisual('borderColor', borderColor); // Only visible series has each data be visual encoded if (!ecModel.isSeriesFiltered(seriesModel)) { if (colorCallback) { data.each(function (idx) { data.setItemVisual( idx, 'color', colorCallback(seriesModel.getDataParams(idx)) ); }); } // itemStyle in each data item var dataEach = function (data, idx) { var itemModel = data.getItemModel(idx); var color = itemModel.get(colorAccessPath, true); var borderColor = itemModel.get(borderColorAccessPath, true); if (color != null) { data.setItemVisual(idx, 'color', color); } if (borderColor != null) { data.setItemVisual(idx, 'borderColor', borderColor); } }; return { dataEach: data.hasItemOption ? dataEach : null }; } } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Language: English. */ var lang = { legend: { selector: { all: 'All', inverse: 'Inv' } }, toolbox: { brush: { title: { rect: 'Box S return { polygon: 'Lasso Select', lineX: 'Horizontally Select', lineY: 'Vertically Select', keep: 'Keep Selections', clear: 'Clear Selections' } }, dataView: { title: 'Data View', lang: ['Data View', 'Close', 'Refresh'] }, dataZoom: { title: { zoom: 'Zoom', back: 'Zoom Reset' } }, magicType: { title: { line: 'Switch to Line Chart', bar: 'Switch to Bar Chart', stack: 'Stack', tiled: 'Tile' } }, restore: { title: 'Restore' }, saveAsImage: { title: 'Save as Image', lang: ['Right Click to Save Image'] } }, series: { typeNames: { pie: 'Pie chart', bar: 'Bar chart', line: 'Line chart', scatter: 'Scatter plot', effectScatter: 'Ripple scatter plot', radar: 'Radar chart', tree: 'Tree', treemap: 'Treemap', boxplot: 'Boxplot', candlestick: 'Candlestick', k: 'K line chart', heatmap: 'Heat map', map: 'Map', parallel: 'Parallel coordinate map', lines: 'Line graph', graph: 'Relationship graph', sankey: 'Sankey diagram', funnel: 'Funnel chart', gauge: 'Guage', pictorialBar: 'Pictorial bar', themeRiver: 'Theme River Map', sunburst: 'Sunburst' } }, aria: { general: { withTitle: 'This is a chart about "{title}"', withoutTitle: 'This is a chart' }, series: { single: { prefix: '', withName: ' with type {seriesType} named {seriesName}.', withoutName: ' with type {seriesType}.' }, multiple: { prefix: '. It consists of {seriesCount} series count.', withName: ' The {seriesId} series is a {seriesType} representing {seriesName}.', withoutName: ' The {seriesId} series is a {seriesType}.', separator: { middle: '', end: '' } } }, data: { allData: 'The data is as follows: ', partialData: 'The first {displayCnt} items are: ', withName: 'the data for {name} is {value}', withoutName: '{value}', separator: { middle: ',', end: '.' } } } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var aria = function (dom, ecModel) { var ariaModel = ecModel.getModel('aria'); if (!ariaModel.get('show')) { return; } else if (ariaModel.get('description')) { dom.setAttribute('aria-label', ariaModel.get('description')); return; } var seriesCnt = 0; ecModel.eachSeries(function (seriesModel, idx) { ++seriesCnt; }, this); var maxDataCnt = ariaModel.get('data.maxCount') || 10; var maxSeriesCnt = ariaModel.get('series.maxCount') || 10; var displaySeriesCnt = Math.min(seriesCnt, maxSeriesCnt); var ariaLabel; if (seriesCnt < 1) { // No series, no aria label return; } else { var title = getTitle(); if (title) { ariaLabel = replace(getConfig('general.withTitle'), { title: title }); } else { ariaLabel = getConfig('general.withoutTitle'); } var seriesLabels = []; var prefix = seriesCnt > 1 ? 'series.multiple.prefix' : 'series.single.prefix'; ariaLabel += replace(getConfig(prefix), { seriesCount: seriesCnt }); ecModel.eachSeries(function (seriesModel, idx) { if (idx < displaySeriesCnt) { var seriesLabel; var seriesName = seriesModel.get('name'); var seriesTpl = 'series.' + (seriesCnt > 1 ? 'multiple' : 'single') + '.'; seriesLabel = getConfig(seriesName ? seriesTpl + 'withName' : seriesTpl + 'withoutName'); seriesLabel = replace(seriesLabel, { seriesId: seriesModel.seriesIndex, seriesName: seriesModel.get('name'), seriesType: getSeriesTypeName(seriesModel.subType) }); var data = seriesModel.getData(); window.data = data; if (data.count() > maxDataCnt) { // Show part of data seriesLabel += replace(getConfig('data.partialData'), { displayCnt: maxDataCnt }); } else { seriesLabel += getConfig('data.allData'); } var dataLabels = []; for (var i = 0; i < data.count(); i++) { if (i < maxDataCnt) { var name = data.getName(i); var value = retrieveRawValue(data, i); dataLabels.push( replace( name ? getConfig('data.withName') : getConfig('data.withoutName'), { name: name, value: value } ) ); } } seriesLabel += dataLabels .join(getConfig('data.separator.middle')) + getConfig('data.separator.end'); seriesLabels.push(seriesLabel); } }); ariaLabel += seriesLabels .join(getConfig('series.multiple.separator.middle')) + getConfig('series.multiple.separator.end'); dom.setAttribute('aria-label', ariaLabel); } function replace(str, keyValues) { if (typeof str !== 'string') { return str; } var result = str; each$1(keyValues, function (value, key) { result = result.replace( new RegExp('\\{\\s*' + key + '\\s*\\}', 'g'), value ); }); return result; } function getConfig(path) { var userConfig = ariaModel.get(path); if (userConfig == null) { var pathArr = path.split('.'); var result = lang.aria; for (var i = 0; i < pathArr.length; ++i) { result = result[pathArr[i]]; } return result; } else { return userConfig; } } function getTitle() { var title = ecModel.getModel('title').option; if (title && title.length) { title = title[0]; } return title && title.text; } function getSeriesTypeName(type) { return lang.series.typeNames[type] || '自定义图'; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var PI$1 = Math.PI; /** * @param {module:echarts/ExtensionAPI} api * @param {Object} [opts] * @param {string} [opts.text] * @param {string} [opts.color] * @param {string} [opts.textColor] * @return {module:zrender/Element} */ var loadingDefault = function (api, opts) { opts = opts || {}; defaults(opts, { text: 'loading', textColor: '#000', fontSize: '12px', maskColor: 'rgba(255, 255, 255, 0.8)', showSpinner: true, color: '#c23531', spinnerRadius: 10, lineWidth: 5, zlevel: 0 }); var group = new Group(); var mask = new Rect({ style: { fill: opts.maskColor }, zlevel: opts.zlevel, z: 10000 }); group.add(mask); var font = opts.fontSize + ' sans-serif'; var labelRect = new Rect({ style: { fill: 'none', text: opts.text, font: font, textPosition: 'right', textDistance: 10, textFill: opts.textColor }, zlevel: opts.zlevel, z: 10001 }); group.add(labelRect); if (opts.showSpinner) { var arc = new Arc({ shape: { startAngle: -PI$1 / 2, endAngle: -PI$1 / 2 + 0.1, r: opts.spinnerRadius }, style: { stroke: opts.color, lineCap: 'round', lineWidth: opts.lineWidth }, zlevel: opts.zlevel, z: 10001 }); arc.animateShape(true) .when(1000, { endAngle: PI$1 * 3 / 2 }) .start('circularInOut'); arc.animateShape(true) .when(1000, { startAngle: PI$1 * 3 / 2 }) .delay(300) .start('circularInOut'); group.add(arc); } // Inject resize group.resize = function () { var textWidth = getWidth(opts.text, font); var r = opts.showSpinner ? opts.spinnerRadius : 0; // cx = (containerWidth - arcDiameter - textDistance - textWidth) / 2 // textDistance needs to be calculated when both animation and text exist var cx = (api.getWidth() - r * 2 - (opts.showSpinner && textWidth ? 10 : 0) - textWidth) / 2 // only show the text - (opts.showSpinner ? 0 : textWidth / 2); var cy = api.getHeight() / 2; opts.showSpinner && arc.setShape({ cx: cx, cy: cy }); labelRect.setShape({ x: cx - r, y: cy - r, width: r * 2, height: r * 2 }); mask.setShape({ x: 0, y: 0, width: api.getWidth(), height: api.getHeight() }); }; group.resize(); return group; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/stream/Scheduler */ /** * @constructor */ function Scheduler(ecInstance, api, dataProcessorHandlers, visualHandlers) { this.ecInstance = ecInstance; this.api = api; this.unfinished; // Fix current processors in case that in some rear cases that // processors might be registered after echarts instance created. // Register processors incrementally for a echarts instance is // not supported by this stream architecture. var dataProcessorHandlers = this._dataProcessorHandlers = dataProcessorHandlers.slice(); var visualHandlers = this._visualHandlers = visualHandlers.slice(); this._allHandlers = dataProcessorHandlers.concat(visualHandlers); /** * @private * @type { * [handlerUID: string]: { * seriesTaskMap?: { * [seriesUID: string]: Task * }, * overallTask?: Task * } * } */ this._stageTaskMap = createHashMap(); } var proto = Scheduler.prototype; /** * @param {module:echarts/model/Global} ecModel * @param {Object} payload */ proto.restoreData = function (ecModel, payload) { // TODO: Only restore needed series and components, but not all components. // Currently `restoreData` of all of the series and component will be called. // But some independent components like `title`, `legend`, `graphic`, `toolbox`, // `tooltip`, `axisPointer`, etc, do not need series refresh when `setOption`, // and some components like coordinate system, axes, dataZoom, visualMap only // need their target series refresh. // (1) If we are implementing this feature some day, we should consider these cases: // if a data processor depends on a component (e.g., dataZoomProcessor depends // on the settings of `dataZoom`), it should be re-performed if the component // is modified by `setOption`. // (2) If a processor depends on sevral series, speicified by its `getTargetSeries`, // it should be re-performed when the result array of `getTargetSeries` changed. // We use `dependencies` to cover these issues. // (3) How to update target series when coordinate system related components modified. // TODO: simply the dirty mechanism? Check whether only the case here can set tasks dirty, // and this case all of the tasks will be set as dirty. ecModel.restoreData(payload); // Theoretically an overall task not only depends on each of its target series, but also // depends on all of the series. // The overall task is not in pipeline, and `ecModel.restoreData` only set pipeline tasks // dirty. If `getTargetSeries` of an overall task returns nothing, we should also ensure // that the overall task is set as dirty and to be performed, otherwise it probably cause // state chaos. So we have to set dirty of all of the overall tasks manually, otherwise it // probably cause state chaos (consider `dataZoomProcessor`). this._stageTaskMap.each(function (taskRecord) { var overallTask = taskRecord.overallTask; overallTask && overallTask.dirty(); }); }; // If seriesModel provided, incremental threshold is check by series data. proto.getPerformArgs = function (task, isBlock) { // For overall task if (!task.__pipeline) { return; } var pipeline = this._pipelineMap.get(task.__pipeline.id); var pCtx = pipeline.context; var incremental = !isBlock && pipeline.progressiveEnabled && (!pCtx || pCtx.progressiveRender) && task.__idxInPipeline > pipeline.blockIndex; var step = incremental ? pipeline.step : null; var modDataCount = pCtx && pCtx.modDataCount; var modBy = modDataCount != null ? Math.ceil(modDataCount / step) : null; return {step: step, modBy: modBy, modDataCount: modDataCount}; }; proto.getPipeline = function (pipelineId) { return this._pipelineMap.get(pipelineId); }; /** * Current, progressive rendering starts from visual and layout. * Always detect render mode in the same stage, avoiding that incorrect * detection caused by data filtering. * Caution: * `updateStreamModes` use `seriesModel.getData()`. */ proto.updateStreamModes = function (seriesModel, view) { var pipeline = this._pipelineMap.get(seriesModel.uid); var data = seriesModel.getData(); var dataLen = data.count(); // `progressiveRender` means that can render progressively in each // animation frame. Note that some types of series do not provide // `view.incrementalPrepareRender` but support `chart.appendData`. We // use the term `incremental` but not `progressive` to describe the // case that `chart.appendData`. var progressiveRender = pipeline.progressiveEnabled && view.incrementalPrepareRender && dataLen >= pipeline.threshold; var large = seriesModel.get('large') && dataLen >= seriesModel.get('largeThreshold'); // TODO: modDataCount should not updated if `appendData`, otherwise cause whole repaint. // see `test/candlestick-large3.html` var modDataCount = seriesModel.get('progressiveChunkMode') === 'mod' ? dataLen : null; seriesModel.pipelineContext = pipeline.context = { progressiveRender: progressiveRender, modDataCount: modDataCount, large: large }; }; proto.restorePipelines = function (ecModel) { var scheduler = this; var pipelineMap = scheduler._pipelineMap = createHashMap(); ecModel.eachSeries(function (seriesModel) { var progressive = seriesModel.getProgressive(); var pipelineId = seriesModel.uid; pipelineMap.set(pipelineId, { id: pipelineId, head: null, tail: null, threshold: seriesModel.getProgressiveThreshold(), progressiveEnabled: progressive && !(seriesModel.preventIncremental && seriesModel.preventIncremental()), blockIndex: -1, step: Math.round(progressive || 700), count: 0 }); pipe(scheduler, seriesModel, seriesModel.dataTask); }); }; proto.prepareStageTasks = function () { var stageTaskMap = this._stageTaskMap; var ecModel = this.ecInstance.getModel(); var api = this.api; each$1(this._allHandlers, function (handler) { var record = stageTaskMap.get(handler.uid) || stageTaskMap.set(handler.uid, []); handler.reset && createSeriesStageTask(this, handler, record, ecModel, api); handler.overallReset && createOverallStageTask(this, handler, record, ecModel, api); }, this); }; proto.prepareView = function (view, model, ecModel, api) { var renderTask = view.renderTask; var context = renderTask.context; context.model = model; context.ecModel = ecModel; context.api = api; renderTask.__block = !view.incrementalPrepareRender; pipe(this, model, renderTask); }; proto.performDataProcessorTasks = function (ecModel, payload) { // If we do not use `block` here, it should be considered when to update modes. performStageTasks(this, this._dataProcessorHandlers, ecModel, payload, {block: true}); }; // opt // opt.visualType: 'visual' or 'layout' // opt.setDirty proto.performVisualTasks = function (ecModel, payload, opt) { performStageTasks(this, this._visualHandlers, ecModel, payload, opt); }; function performStageTasks(scheduler, stageHandlers, ecModel, payload, opt) { opt = opt || {}; var unfinished; each$1(stageHandlers, function (stageHandler, idx) { if (opt.visualType && opt.visualType !== stageHandler.visualType) { return; } var stageHandlerRecord = scheduler._stageTaskMap.get(stageHandler.uid); var seriesTaskMap = stageHandlerRecord.seriesTaskMap; var overallTask = stageHandlerRecord.overallTask; if (overallTask) { var overallNeedDirty; var agentStubMap = overallTask.agentStubMap; agentStubMap.each(function (stub) { if (needSetDirty(opt, stub)) { stub.dirty(); overallNeedDirty = true; } }); overallNeedDirty && overallTask.dirty(); updatePayload(overallTask, payload); var performArgs = scheduler.getPerformArgs(overallTask, opt.block); // Execute stubs firstly, which may set the overall task dirty, // then execute the overall task. And stub will call seriesModel.setData, // which ensures that in the overallTask seriesModel.getData() will not // return incorrect data. agentStubMap.each(function (stub) { stub.perform(performArgs); }); unfinished |= overallTask.perform(performArgs); } else if (seriesTaskMap) { seriesTaskMap.each(function (task, pipelineId) { if (needSetDirty(opt, task)) { task.dirty(); } var performArgs = scheduler.getPerformArgs(task, opt.block); // FIXME // if intending to decalare `performRawSeries` in handlers, only // stream-independent (specifically, data item independent) operations can be // performed. Because is a series is filtered, most of the tasks will not // be performed. A stream-dependent operation probably cause wrong biz logic. // Perhaps we should not provide a separate callback for this case instead // of providing the config `performRawSeries`. The stream-dependent operaions // and stream-independent operations should better not be mixed. performArgs.skip = !stageHandler.performRawSeries && ecModel.isSeriesFiltered(task.context.model); updatePayload(task, payload); unfinished |= task.perform(performArgs); }); } }); function needSetDirty(opt, task) { return opt.setDirty && (!opt.dirtyMap || opt.dirtyMap.get(task.__pipeline.id)); } scheduler.unfinished |= unfinished; } proto.performSeriesTasks = function (ecModel) { var unfinished; ecModel.eachSeries(function (seriesModel) { // Progress to the end for dataInit and dataRestore. unfinished |= seriesModel.dataTask.perform(); }); this.unfinished |= unfinished; }; proto.plan = function () { // Travel pipelines, check block. this._pipelineMap.each(function (pipeline) { var task = pipeline.tail; do { if (task.__block) { pipeline.blockIndex = task.__idxInPipeline; break; } task = task.getUpstream(); } while (task); }); }; var updatePayload = proto.updatePayload = function (task, payload) { payload !== 'remain' && (task.context.payload = payload); }; function createSeriesStageTask(scheduler, stageHandler, stageHandlerRecord, ecModel, api) { var seriesTaskMap = stageHandlerRecord.seriesTaskMap || (stageHandlerRecord.seriesTaskMap = createHashMap()); var seriesType = stageHandler.seriesType; var getTargetSeries = stageHandler.getTargetSeries; // If a stageHandler should cover all series, `createOnAllSeries` should be declared mandatorily, // to avoid some typo or abuse. Otherwise if an extension do not specify a `seriesType`, // it works but it may cause other irrelevant charts blocked. if (stageHandler.createOnAllSeries) { ecModel.eachRawSeries(create); } else if (seriesType) { ecModel.eachRawSeriesByType(seriesType, create); } else if (getTargetSeries) { getTargetSeries(ecModel, api).each(create); } function create(seriesModel) { var pipelineId = seriesModel.uid; // Init tasks for each seriesModel only once. // Reuse original task instance. var task = seriesTaskMap.get(pipelineId) || seriesTaskMap.set(pipelineId, createTask({ plan: seriesTaskPlan, reset: seriesTaskReset, count: seriesTaskCount })); task.context = { model: seriesModel, ecModel: ecModel, api: api, useClearVisual: stageHandler.isVisual && !stageHandler.isLayout, plan: stageHandler.plan, reset: stageHandler.reset, scheduler: scheduler }; pipe(scheduler, seriesModel, task); } // Clear unused series tasks. var pipelineMap = scheduler._pipelineMap; seriesTaskMap.each(function (task, pipelineId) { if (!pipelineMap.get(pipelineId)) { task.dispose(); seriesTaskMap.removeKey(pipelineId); } }); } function createOverallStageTask(scheduler, stageHandler, stageHandlerRecord, ecModel, api) { var overallTask = stageHandlerRecord.overallTask = stageHandlerRecord.overallTask // For overall task, the function only be called on reset stage. || createTask({reset: overallTaskReset}); overallTask.context = { ecModel: ecModel, api: api, overallReset: stageHandler.overallReset, scheduler: scheduler }; // Reuse orignal stubs. var agentStubMap = overallTask.agentStubMap = overallTask.agentStubMap || createHashMap(); var seriesType = stageHandler.seriesType; var getTargetSeries = stageHandler.getTargetSeries; var overallProgress = true; var modifyOutputEnd = stageHandler.modifyOutputEnd; // An overall task with seriesType detected or has `getTargetSeries`, we add // stub in each pipelines, it will set the overall task dirty when the pipeline // progress. Moreover, to avoid call the overall task each frame (too frequent), // we set the pipeline block. if (seriesType) { ecModel.eachRawSeriesByType(seriesType, createStub); } else if (getTargetSeries) { getTargetSeries(ecModel, api).each(createStub); } // Otherwise, (usually it is legancy case), the overall task will only be // executed when upstream dirty. Otherwise the progressive rendering of all // pipelines will be disabled unexpectedly. But it still needs stubs to receive // dirty info from upsteam. else { overallProgress = false; each$1(ecModel.getSeries(), createStub); } function createStub(seriesModel) { var pipelineId = seriesModel.uid; var stub = agentStubMap.get(pipelineId); if (!stub) { stub = agentStubMap.set(pipelineId, createTask( {reset: stubReset, onDirty: stubOnDirty} )); // When the result of `getTargetSeries` changed, the overallTask // should be set as dirty and re-performed. overallTask.dirty(); } stub.context = { model: seriesModel, overallProgress: overallProgress, modifyOutputEnd: modifyOutputEnd }; stub.agent = overallTask; stub.__block = overallProgress; pipe(scheduler, seriesModel, stub); } // Clear unused stubs. var pipelineMap = scheduler._pipelineMap; agentStubMap.each(function (stub, pipelineId) { if (!pipelineMap.get(pipelineId)) { stub.dispose(); // When the result of `getTargetSeries` changed, the overallTask // should be set as dirty and re-performed. overallTask.dirty(); agentStubMap.removeKey(pipelineId); } }); } function overallTaskReset(context) { context.overallReset( context.ecModel, context.api, context.payload ); } function stubReset(context, upstreamContext) { return context.overallProgress && stubProgress; } function stubProgress() { this.agent.dirty(); this.getDownstream().dirty(); } function stubOnDirty() { this.agent && this.agent.dirty(); } function seriesTaskPlan(context) { return context.plan && context.plan( context.model, context.ecModel, context.api, context.payload ); } function seriesTaskReset(context) { if (context.useClearVisual) { context.data.clearAllVisual(); } var resetDefines = context.resetDefines = normalizeToArray(context.reset( context.model, context.ecModel, context.api, context.payload )); return resetDefines.length > 1 ? map(resetDefines, function (v, idx) { return makeSeriesTaskProgress(idx); }) : singleSeriesTaskProgress; } var singleSeriesTaskProgress = makeSeriesTaskProgress(0); function makeSeriesTaskProgress(resetDefineIdx) { return function (params, context) { var data = context.data; var resetDefine = context.resetDefines[resetDefineIdx]; if (resetDefine && resetDefine.dataEach) { for (var i = params.start; i < params.end; i++) { resetDefine.dataEach(data, i); } } else if (resetDefine && resetDefine.progress) { resetDefine.progress(params, data); } }; } function seriesTaskCount(context) { return context.data.count(); } function pipe(scheduler, seriesModel, task) { var pipelineId = seriesModel.uid; var pipeline = scheduler._pipelineMap.get(pipelineId); !pipeline.head && (pipeline.head = task); pipeline.tail && pipeline.tail.pipe(task); pipeline.tail = task; task.__idxInPipeline = pipeline.count++; task.__pipeline = pipeline; } Scheduler.wrapStageHandler = function (stageHandler, visualType) { if (isFunction$1(stageHandler)) { stageHandler = { overallReset: stageHandler, seriesType: detectSeriseType(stageHandler) }; } stageHandler.uid = getUID('stageHandler'); visualType && (stageHandler.visualType = visualType); return stageHandler; }; /** * Only some legacy stage handlers (usually in echarts extensions) are pure function. * To ensure that they can work normally, they should work in block mode, that is, * they should not be started util the previous tasks finished. So they cause the * progressive rendering disabled. We try to detect the series type, to narrow down * the block range to only the series type they concern, but not all series. */ function detectSeriseType(legacyFunc) { seriesType = null; try { // Assume there is no async when calling `eachSeriesByType`. legacyFunc(ecModelMock, apiMock); } catch (e) { } return seriesType; } var ecModelMock = {}; var apiMock = {}; var seriesType; mockMethods(ecModelMock, GlobalModel); mockMethods(apiMock, ExtensionAPI); ecModelMock.eachSeriesByType = ecModelMock.eachRawSeriesByType = function (type) { seriesType = type; }; ecModelMock.eachComponent = function (cond) { if (cond.mainType === 'series' && cond.subType) { seriesType = cond.subType; } }; function mockMethods(target, Clz) { /* eslint-disable */ for (var name in Clz.prototype) { // Do not use hasOwnProperty target[name] = noop; } /* eslint-enable */ } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var colorAll = [ '#37A2DA', '#32C5E9', '#67E0E3', '#9FE6B8', '#FFDB5C', '#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF' ]; var lightTheme = { color: colorAll, colorLayer: [ ['#37A2DA', '#ffd85c', '#fd7b5f'], ['#37A2DA', '#67E0E3', '#FFDB5C', '#ff9f7f', '#E062AE', '#9d96f5'], ['#37A2DA', '#32C5E9', '#9FE6B8', '#FFDB5C', '#ff9f7f', '#fb7293', '#e7bcf3', '#8378EA', '#96BFFF'], colorAll ] }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var contrastColor = '#eee'; var axisCommon = function () { return { axisLine: { lineStyle: { color: contrastColor } }, axisTick: { lineStyle: { color: contrastColor } }, axisLabel: { textStyle: { color: contrastColor } }, splitLine: { lineStyle: { type: 'dashed', color: '#aaa' } }, splitArea: { areaStyle: { color: contrastColor } } }; }; var colorPalette = [ '#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', '#eedd78', '#73a373', '#73b9bc', '#7289ab', '#91ca8c', '#f49f42' ]; var theme = { color: colorPalette, backgroundColor: '#333', tooltip: { axisPointer: { lineStyle: { color: contrastColor }, crossStyle: { color: contrastColor }, label: { color: '#000' } } }, legend: { textStyle: { color: contrastColor } }, textStyle: { color: contrastColor }, title: { textStyle: { color: contrastColor } }, toolbox: { iconStyle: { normal: { borderColor: contrastColor } } }, dataZoom: { textStyle: { color: contrastColor } }, visualMap: { textStyle: { color: contrastColor } }, timeline: { lineStyle: { color: contrastColor }, itemStyle: { normal: { color: colorPalette[1] } }, label: { normal: { textStyle: { color: contrastColor } } }, controlStyle: { normal: { color: contrastColor, borderColor: contrastColor } } }, timeAxis: axisCommon(), logAxis: axisCommon(), valueAxis: axisCommon(), categoryAxis: axisCommon(), line: { symbol: 'circle' }, graph: { color: colorPalette }, gauge: { title: { textStyle: { color: contrastColor } } }, candlestick: { itemStyle: { normal: { color: '#FD1050', color0: '#0CF49B', borderColor: '#FD1050', borderColor0: '#0CF49B' } } } }; theme.categoryAxis.splitLine.show = false; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * This module is imported by echarts directly. * * Notice: * Always keep this file exists for backward compatibility. * Because before 4.1.0, dataset is an optional component, * some users may import this module manually. */ ComponentModel.extend({ type: 'dataset', /** * @protected */ defaultOption: { // 'row', 'column' seriesLayoutBy: SERIES_LAYOUT_BY_COLUMN, // null/'auto': auto detect header, see "module:echarts/data/helper/sourceHelper" sourceHeader: null, dimensions: null, source: null }, optionUpdated: function () { detectSourceFormat(this); } }); Component$1.extend({ type: 'dataset' }); /** * 椭圆形状 * @module zrender/graphic/shape/Ellipse */ var Ellipse = Path.extend({ type: 'ellipse', shape: { cx: 0, cy: 0, rx: 0, ry: 0 }, buildPath: function (ctx, shape) { var k = 0.5522848; var x = shape.cx; var y = shape.cy; var a = shape.rx; var b = shape.ry; var ox = a * k; // 水平控制点偏移量 var oy = b * k; // 垂直控制点偏移量 // 从椭圆的左端点开始顺时针绘制四条三次贝塞尔曲线 ctx.moveTo(x - a, y); ctx.bezierCurveTo(x - a, y - oy, x - ox, y - b, x, y - b); ctx.bezierCurveTo(x + ox, y - b, x + a, y - oy, x + a, y); ctx.bezierCurveTo(x + a, y + oy, x + ox, y + b, x, y + b); ctx.bezierCurveTo(x - ox, y + b, x - a, y + oy, x - a, y); ctx.closePath(); } }); // import RadialGradient from '../graphic/RadialGradient'; // import Pattern from '../graphic/Pattern'; // import * as vector from '../core/vector'; // Most of the values can be separated by comma and/or white space. var DILIMITER_REG = /[\s,]+/; /** * For big svg string, this method might be time consuming. * * @param {string} svg xml string * @return {Object} xml root. */ function parseXML(svg) { if (isString(svg)) { var parser = new DOMParser(); svg = parser.parseFromString(svg, 'text/xml'); } // Document node. If using $.get, doc node may be input. if (svg.nodeType === 9) { svg = svg.firstChild; } // nodeName of is also 'svg'. while (svg.nodeName.toLowerCase() !== 'svg' || svg.nodeType !== 1) { svg = svg.nextSibling; } return svg; } function SVGParser() { this._defs = {}; this._root = null; this._isDefine = false; this._isText = false; } SVGParser.prototype.parse = function (xml, opt) { opt = opt || {}; var svg = parseXML(xml); if (!svg) { throw new Error('Illegal svg'); } var root = new Group(); this._root = root; // parse view port var viewBox = svg.getAttribute('viewBox') || ''; // If width/height not specified, means "100%" of `opt.width/height`. // TODO: Other percent value not supported yet. var width = parseFloat(svg.getAttribute('width') || opt.width); var height = parseFloat(svg.getAttribute('height') || opt.height); // If width/height not specified, set as null for output. isNaN(width) && (width = null); isNaN(height) && (height = null); // Apply inline style on svg element. parseAttributes(svg, root, null, true); var child = svg.firstChild; while (child) { this._parseNode(child, root); child = child.nextSibling; } var viewBoxRect; var viewBoxTransform; if (viewBox) { var viewBoxArr = trim(viewBox).split(DILIMITER_REG); // Some invalid case like viewBox: 'none'. if (viewBoxArr.length >= 4) { viewBoxRect = { x: parseFloat(viewBoxArr[0] || 0), y: parseFloat(viewBoxArr[1] || 0), width: parseFloat(viewBoxArr[2]), height: parseFloat(viewBoxArr[3]) }; } } if (viewBoxRect && width != null && height != null) { viewBoxTransform = makeViewBoxTransform(viewBoxRect, width, height); if (!opt.ignoreViewBox) { // If set transform on the output group, it probably bring trouble when // some users only intend to show the clipped content inside the viewBox, // but not intend to transform the output group. So we keep the output // group no transform. If the user intend to use the viewBox as a // camera, just set `opt.ignoreViewBox` as `true` and set transfrom // manually according to the viewBox info in the output of this method. var elRoot = root; root = new Group(); root.add(elRoot); elRoot.scale = viewBoxTransform.scale.slice(); elRoot.position = viewBoxTransform.position.slice(); } } // Some shapes might be overflow the viewport, which should be // clipped despite whether the viewBox is used, as the SVG does. if (!opt.ignoreRootClip && width != null && height != null) { root.setClipPath(new Rect({ shape: {x: 0, y: 0, width: width, height: height} })); } // Set width/height on group just for output the viewport size. return { root: root, width: width, height: height, viewBoxRect: viewBoxRect, viewBoxTransform: viewBoxTransform }; }; SVGParser.prototype._parseNode = function (xmlNode, parentGroup) { var nodeName = xmlNode.nodeName.toLowerCase(); // TODO // support in svg, where nodeName is 'style', // CSS classes is defined globally wherever the style tags are declared. if (nodeName === 'defs') { // define flag this._isDefine = true; } else if (nodeName === 'text') { this._isText = true; } var el; if (this._isDefine) { var parser = defineParsers[nodeName]; if (parser) { var def = parser.call(this, xmlNode); var id = xmlNode.getAttribute('id'); if (id) { this._defs[id] = def; } } } else { var parser = nodeParsers[nodeName]; if (parser) { el = parser.call(this, xmlNode, parentGroup); parentGroup.add(el); } } var child = xmlNode.firstChild; while (child) { if (child.nodeType === 1) { this._parseNode(child, el); } // Is text if (child.nodeType === 3 && this._isText) { this._parseText(child, el); } child = child.nextSibling; } // Quit define if (nodeName === 'defs') { this._isDefine = false; } else if (nodeName === 'text') { this._isText = false; } }; SVGParser.prototype._parseText = function (xmlNode, parentGroup) { if (xmlNode.nodeType === 1) { var dx = xmlNode.getAttribute('dx') || 0; var dy = xmlNode.getAttribute('dy') || 0; this._textX += parseFloat(dx); this._textY += parseFloat(dy); } var text = new Text({ style: { text: xmlNode.textContent, transformText: true }, position: [this._textX || 0, this._textY || 0] }); inheritStyle(parentGroup, text); parseAttributes(xmlNode, text, this._defs); var fontSize = text.style.fontSize; if (fontSize && fontSize < 9) { // PENDING text.style.fontSize = 9; text.scale = text.scale || [1, 1]; text.scale[0] *= fontSize / 9; text.scale[1] *= fontSize / 9; } var rect = text.getBoundingRect(); this._textX += rect.width; parentGroup.add(text); return text; }; var nodeParsers = { 'g': function (xmlNode, parentGroup) { var g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defs); return g; }, 'rect': function (xmlNode, parentGroup) { var rect = new Rect(); inheritStyle(parentGroup, rect); parseAttributes(xmlNode, rect, this._defs); rect.setShape({ x: parseFloat(xmlNode.getAttribute('x') || 0), y: parseFloat(xmlNode.getAttribute('y') || 0), width: parseFloat(xmlNode.getAttribute('width') || 0), height: parseFloat(xmlNode.getAttribute('height') || 0) }); // console.log(xmlNode.getAttribute('transform')); // console.log(rect.transform); return rect; }, 'circle': function (xmlNode, parentGroup) { var circle = new Circle(); inheritStyle(parentGroup, circle); parseAttributes(xmlNode, circle, this._defs); circle.setShape({ cx: parseFloat(xmlNode.getAttribute('cx') || 0), cy: parseFloat(xmlNode.getAttribute('cy') || 0), r: parseFloat(xmlNode.getAttribute('r') || 0) }); return circle; }, 'line': function (xmlNode, parentGroup) { var line = new Line(); inheritStyle(parentGroup, line); parseAttributes(xmlNode, line, this._defs); line.setShape({ x1: parseFloat(xmlNode.getAttribute('x1') || 0), y1: parseFloat(xmlNode.getAttribute('y1') || 0), x2: parseFloat(xmlNode.getAttribute('x2') || 0), y2: parseFloat(xmlNode.getAttribute('y2') || 0) }); return line; }, 'ellipse': function (xmlNode, parentGroup) { var ellipse = new Ellipse(); inheritStyle(parentGroup, ellipse); parseAttributes(xmlNode, ellipse, this._defs); ellipse.setShape({ cx: parseFloat(xmlNode.getAttribute('cx') || 0), cy: parseFloat(xmlNode.getAttribute('cy') || 0), rx: parseFloat(xmlNode.getAttribute('rx') || 0), ry: parseFloat(xmlNode.getAttribute('ry') || 0) }); return ellipse; }, 'polygon': function (xmlNode, parentGroup) { var points = xmlNode.getAttribute('points'); if (points) { points = parsePoints(points); } var polygon = new Polygon({ shape: { points: points || [] } }); inheritStyle(parentGroup, polygon); parseAttributes(xmlNode, polygon, this._defs); return polygon; }, 'polyline': function (xmlNode, parentGroup) { var path = new Path(); inheritStyle(parentGroup, path); parseAttributes(xmlNode, path, this._defs); var points = xmlNode.getAttribute('points'); if (points) { points = parsePoints(points); } var polyline = new Polyline({ shape: { points: points || [] } }); return polyline; }, 'image': function (xmlNode, parentGroup) { var img = new ZImage(); inheritStyle(parentGroup, img); parseAttributes(xmlNode, img, this._defs); img.setStyle({ image: xmlNode.getAttribute('xlink:href'), x: xmlNode.getAttribute('x'), y: xmlNode.getAttribute('y'), width: xmlNode.getAttribute('width'), height: xmlNode.getAttribute('height') }); return img; }, 'text': function (xmlNode, parentGroup) { var x = xmlNode.getAttribute('x') || 0; var y = xmlNode.getAttribute('y') || 0; var dx = xmlNode.getAttribute('dx') || 0; var dy = xmlNode.getAttribute('dy') || 0; this._textX = parseFloat(x) + parseFloat(dx); this._textY = parseFloat(y) + parseFloat(dy); var g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defs); return g; }, 'tspan': function (xmlNode, parentGroup) { var x = xmlNode.getAttribute('x'); var y = xmlNode.getAttribute('y'); if (x != null) { // new offset x this._textX = parseFloat(x); } if (y != null) { // new offset y this._textY = parseFloat(y); } var dx = xmlNode.getAttribute('dx') || 0; var dy = xmlNode.getAttribute('dy') || 0; var g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defs); this._textX += dx; this._textY += dy; return g; }, 'path': function (xmlNode, parentGroup) { // TODO svg fill rule // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule // path.style.globalCompositeOperation = 'xor'; var d = xmlNode.getAttribute('d') || ''; // Performance sensitive. var path = createFromString(d); inheritStyle(parentGroup, path); parseAttributes(xmlNode, path, this._defs); return path; } }; var defineParsers = { 'lineargradient': function (xmlNode) { var x1 = parseInt(xmlNode.getAttribute('x1') || 0, 10); var y1 = parseInt(xmlNode.getAttribute('y1') || 0, 10); var x2 = parseInt(xmlNode.getAttribute('x2') || 10, 10); var y2 = parseInt(xmlNode.getAttribute('y2') || 0, 10); var gradient = new LinearGradient(x1, y1, x2, y2); _parseGradientColorStops(xmlNode, gradient); return gradient; }, 'radialgradient': function (xmlNode) { } }; function _parseGradientColorStops(xmlNode, gradient) { var stop = xmlNode.firstChild; while (stop) { if (stop.nodeType === 1) { var offset = stop.getAttribute('offset'); if (offset.indexOf('%') > 0) { // percentage offset = parseInt(offset, 10) / 100; } else if (offset) { // number from 0 to 1 offset = parseFloat(offset); } else { offset = 0; } var stopColor = stop.getAttribute('stop-color') || '#000000'; gradient.addColorStop(offset, stopColor); } stop = stop.nextSibling; } } function inheritStyle(parent, child) { if (parent && parent.__inheritedStyle) { if (!child.__inheritedStyle) { child.__inheritedStyle = {}; } defaults(child.__inheritedStyle, parent.__inheritedStyle); } } function parsePoints(pointsString) { var list = trim(pointsString).split(DILIMITER_REG); var points = []; for (var i = 0; i < list.length; i += 2) { var x = parseFloat(list[i]); var y = parseFloat(list[i + 1]); points.push([x, y]); } return points; } var attributesMap = { 'fill': 'fill', 'stroke': 'stroke', 'stroke-width': 'lineWidth', 'opacity': 'opacity', 'fill-opacity': 'fillOpacity', 'stroke-opacity': 'strokeOpacity', 'stroke-dasharray': 'lineDash', 'stroke-dashoffset': 'lineDashOffset', 'stroke-linecap': 'lineCap', 'stroke-linejoin': 'lineJoin', 'stroke-miterlimit': 'miterLimit', 'font-family': 'fontFamily', 'font-size': 'fontSize', 'font-style': 'fontStyle', 'font-weight': 'fontWeight', 'text-align': 'textAlign', 'alignment-baseline': 'textBaseline' }; function parseAttributes(xmlNode, el, defs, onlyInlineStyle) { var zrStyle = el.__inheritedStyle || {}; var isTextEl = el.type === 'text'; // TODO Shadow if (xmlNode.nodeType === 1) { parseTransformAttribute(xmlNode, el); extend(zrStyle, parseStyleAttribute(xmlNode)); if (!onlyInlineStyle) { for (var svgAttrName in attributesMap) { if (attributesMap.hasOwnProperty(svgAttrName)) { var attrValue = xmlNode.getAttribute(svgAttrName); if (attrValue != null) { zrStyle[attributesMap[svgAttrName]] = attrValue; } } } } } var elFillProp = isTextEl ? 'textFill' : 'fill'; var elStrokeProp = isTextEl ? 'textStroke' : 'stroke'; el.style = el.style || new Style(); var elStyle = el.style; zrStyle.fill != null && elStyle.set(elFillProp, getPaint(zrStyle.fill, defs)); zrStyle.stroke != null && elStyle.set(elStrokeProp, getPaint(zrStyle.stroke, defs)); each$1([ 'lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize' ], function (propName) { var elPropName = (propName === 'lineWidth' && isTextEl) ? 'textStrokeWidth' : propName; zrStyle[propName] != null && elStyle.set(elPropName, parseFloat(zrStyle[propName])); }); if (!zrStyle.textBaseline || zrStyle.textBaseline === 'auto') { zrStyle.textBaseline = 'alphabetic'; } if (zrStyle.textBaseline === 'alphabetic') { zrStyle.textBaseline = 'bottom'; } if (zrStyle.textAlign === 'start') { zrStyle.textAlign = 'left'; } if (zrStyle.textAlign === 'end') { zrStyle.textAlign = 'right'; } each$1(['lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign', 'textBaseline' ], function (propName) { zrStyle[propName] != null && elStyle.set(propName, zrStyle[propName]); }); if (zrStyle.lineDash) { el.style.lineDash = trim(zrStyle.lineDash).split(DILIMITER_REG); } if (elStyle[elStrokeProp] && elStyle[elStrokeProp] !== 'none') { // enable stroke el[elStrokeProp] = true; } el.__inheritedStyle = zrStyle; } var urlRegex = /url\(\s*#(.*?)\)/; function getPaint(str, defs) { // if (str === 'none') { // return; // } var urlMatch = defs && str && str.match(urlRegex); if (urlMatch) { var url = trim(urlMatch[1]); var def = defs[url]; return def; } return str; } var transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.e,]*)\)/g; function parseTransformAttribute(xmlNode, node) { var transform = xmlNode.getAttribute('transform'); if (transform) { transform = transform.replace(/,/g, ' '); var m = null; var transformOps = []; transform.replace(transformRegex, function (str, type, value) { transformOps.push(type, value); }); for (var i = transformOps.length - 1; i > 0; i -= 2) { var value = transformOps[i]; var type = transformOps[i - 1]; m = m || create$1(); switch (type) { case 'translate': value = trim(value).split(DILIMITER_REG); translate(m, m, [parseFloat(value[0]), parseFloat(value[1] || 0)]); break; case 'scale': value = trim(value).split(DILIMITER_REG); scale$1(m, m, [parseFloat(value[0]), parseFloat(value[1] || value[0])]); break; case 'rotate': value = trim(value).split(DILIMITER_REG); rotate(m, m, parseFloat(value[0])); break; case 'skew': value = trim(value).split(DILIMITER_REG); console.warn('Skew transform is not supported yet'); break; case 'matrix': var value = trim(value).split(DILIMITER_REG); m[0] = parseFloat(value[0]); m[1] = parseFloat(value[1]); m[2] = parseFloat(value[2]); m[3] = parseFloat(value[3]); m[4] = parseFloat(value[4]); m[5] = parseFloat(value[5]); break; } } node.setLocalTransform(m); } } // Value may contain space. var styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g; function parseStyleAttribute(xmlNode) { var style = xmlNode.getAttribute('style'); var result = {}; if (!style) { return result; } var styleList = {}; styleRegex.lastIndex = 0; var styleRegResult; while ((styleRegResult = styleRegex.exec(style)) != null) { styleList[styleRegResult[1]] = styleRegResult[2]; } for (var svgAttrName in attributesMap) { if (attributesMap.hasOwnProperty(svgAttrName) && styleList[svgAttrName] != null) { result[attributesMap[svgAttrName]] = styleList[svgAttrName]; } } return result; } /** * @param {Array.} viewBoxRect * @param {number} width * @param {number} height * @return {Object} {scale, position} */ function makeViewBoxTransform(viewBoxRect, width, height) { var scaleX = width / viewBoxRect.width; var scaleY = height / viewBoxRect.height; var scale = Math.min(scaleX, scaleY); // preserveAspectRatio 'xMidYMid' var viewBoxScale = [scale, scale]; var viewBoxPosition = [ -(viewBoxRect.x + viewBoxRect.width / 2) * scale + width / 2, -(viewBoxRect.y + viewBoxRect.height / 2) * scale + height / 2 ]; return { scale: viewBoxScale, position: viewBoxPosition }; } /** * @param {string|XMLElement} xml * @param {Object} [opt] * @param {number} [opt.width] Default width if svg width not specified or is a percent value. * @param {number} [opt.height] Default height if svg height not specified or is a percent value. * @param {boolean} [opt.ignoreViewBox] * @param {boolean} [opt.ignoreRootClip] * @return {Object} result: * { * root: Group, The root of the the result tree of zrender shapes, * width: number, the viewport width of the SVG, * height: number, the viewport height of the SVG, * viewBoxRect: {x, y, width, height}, the declared viewBox rect of the SVG, if exists, * viewBoxTransform: the {scale, position} calculated by viewBox and viewport, is exists. * } */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var storage = createHashMap(); // For minimize the code size of common echarts package, // do not put too much logic in this module. var mapDataStorage = { // The format of record: see `echarts.registerMap`. // Compatible with previous `echarts.registerMap`. registerMap: function (mapName, rawGeoJson, rawSpecialAreas) { var records; if (isArray(rawGeoJson)) { records = rawGeoJson; } else if (rawGeoJson.svg) { records = [{ type: 'svg', source: rawGeoJson.svg, specialAreas: rawGeoJson.specialAreas }]; } else { // Backward compatibility. if (rawGeoJson.geoJson && !rawGeoJson.features) { rawSpecialAreas = rawGeoJson.specialAreas; rawGeoJson = rawGeoJson.geoJson; } records = [{ type: 'geoJSON', source: rawGeoJson, specialAreas: rawSpecialAreas }]; } each$1(records, function (record) { var type = record.type; type === 'geoJson' && (type = record.type = 'geoJSON'); var parse = parsers[type]; if (__DEV__) { assert$1(parse, 'Illegal map type: ' + type); } parse(record); }); return storage.set(mapName, records); }, retrieveMap: function (mapName) { return storage.get(mapName); } }; var parsers = { geoJSON: function (record) { var source = record.source; record.geoJSON = !isString(source) ? source : (typeof JSON !== 'undefined' && JSON.parse) ? JSON.parse(source) : (new Function('return (' + source + ');'))(); }, // Only perform parse to XML object here, which might be time // consiming for large SVG. // Although convert XML to zrender element is also time consiming, // if we do it here, the clone of zrender elements has to be // required. So we do it once for each geo instance, util real // performance issues call for optimizing it. svg: function (record) { record.svgXML = parseXML(record.source); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var assert = assert$1; var each = each$1; var isFunction = isFunction$1; var isObject = isObject$1; var parseClassType = ComponentModel.parseClassType; var version = '4.9.0'; var dependencies = { zrender: '4.3.2' }; var TEST_FRAME_REMAIN_TIME = 1; var PRIORITY_PROCESSOR_FILTER = 1000; var PRIORITY_PROCESSOR_SERIES_FILTER = 800; var PRIORITY_PROCESSOR_DATASTACK = 900; var PRIORITY_PROCESSOR_STATISTIC = 5000; var PRIORITY_VISUAL_LAYOUT = 1000; var PRIORITY_VISUAL_PROGRESSIVE_LAYOUT = 1100; var PRIORITY_VISUAL_GLOBAL = 2000; var PRIORITY_VISUAL_CHART = 3000; var PRIORITY_VISUAL_POST_CHART_LAYOUT = 3500; var PRIORITY_VISUAL_COMPONENT = 4000; // FIXME // necessary? var PRIORITY_VISUAL_BRUSH = 5000; var PRIORITY = { PROCESSOR: { FILTER: PRIORITY_PROCESSOR_FILTER, SERIES_FILTER: PRIORITY_PROCESSOR_SERIES_FILTER, STATISTIC: PRIORITY_PROCESSOR_STATISTIC }, VISUAL: { LAYOUT: PRIORITY_VISUAL_LAYOUT, PROGRESSIVE_LAYOUT: PRIORITY_VISUAL_PROGRESSIVE_LAYOUT, GLOBAL: PRIORITY_VISUAL_GLOBAL, CHART: PRIORITY_VISUAL_CHART, POST_CHART_LAYOUT: PRIORITY_VISUAL_POST_CHART_LAYOUT, COMPONENT: PRIORITY_VISUAL_COMPONENT, BRUSH: PRIORITY_VISUAL_BRUSH } }; // Main process have three entries: `setOption`, `dispatchAction` and `resize`, // where they must not be invoked nestedly, except the only case: invoke // dispatchAction with updateMethod "none" in main process. // This flag is used to carry out this rule. // All events will be triggered out side main process (i.e. when !this[IN_MAIN_PROCESS]). var IN_MAIN_PROCESS = '__flagInMainProcess'; var OPTION_UPDATED = '__optionUpdated'; var ACTION_REG = /^[a-zA-Z0-9_]+$/; function createRegisterEventWithLowercaseName(method, ignoreDisposed) { return function (eventName, handler, context) { if (!ignoreDisposed && this._disposed) { disposedWarning(this.id); return; } // Event name is all lowercase eventName = eventName && eventName.toLowerCase(); Eventful.prototype[method].call(this, eventName, handler, context); }; } /** * @module echarts~MessageCenter */ function MessageCenter() { Eventful.call(this); } MessageCenter.prototype.on = createRegisterEventWithLowercaseName('on', true); MessageCenter.prototype.off = createRegisterEventWithLowercaseName('off', true); MessageCenter.prototype.one = createRegisterEventWithLowercaseName('one', true); mixin(MessageCenter, Eventful); /** * @module echarts~ECharts */ function ECharts(dom, theme$$1, opts) { opts = opts || {}; // Get theme by name if (typeof theme$$1 === 'string') { theme$$1 = themeStorage[theme$$1]; } /** * @type {string} */ this.id; /** * Group id * @type {string} */ this.group; /** * @type {HTMLElement} * @private */ this._dom = dom; var defaultRenderer = 'canvas'; if (__DEV__) { defaultRenderer = ( typeof window === 'undefined' ? global : window ).__ECHARTS__DEFAULT__RENDERER__ || defaultRenderer; } /** * @type {module:zrender/ZRender} * @private */ var zr = this._zr = init$1(dom, { renderer: opts.renderer || defaultRenderer, devicePixelRatio: opts.devicePixelRatio, width: opts.width, height: opts.height }); /** * Expect 60 fps. * @type {Function} * @private */ this._throttledZrFlush = throttle(bind(zr.flush, zr), 17); var theme$$1 = clone(theme$$1); theme$$1 && backwardCompat(theme$$1, true); /** * @type {Object} * @private */ this._theme = theme$$1; /** * @type {Array.} * @private */ this._chartsViews = []; /** * @type {Object.} * @private */ this._chartsMap = {}; /** * @type {Array.} * @private */ this._componentsViews = []; /** * @type {Object.} * @private */ this._componentsMap = {}; /** * @type {module:echarts/CoordinateSystem} * @private */ this._coordSysMgr = new CoordinateSystemManager(); /** * @type {module:echarts/ExtensionAPI} * @private */ var api = this._api = createExtensionAPI(this); // Sort on demand function prioritySortFunc(a, b) { return a.__prio - b.__prio; } sort(visualFuncs, prioritySortFunc); sort(dataProcessorFuncs, prioritySortFunc); /** * @type {module:echarts/stream/Scheduler} */ this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs); Eventful.call(this, this._ecEventProcessor = new EventProcessor()); /** * @type {module:echarts~MessageCenter} * @private */ this._messageCenter = new MessageCenter(); // Init mouse events this._initEvents(); // In case some people write `window.onresize = chart.resize` this.resize = bind(this.resize, this); // Can't dispatch action during rendering procedure this._pendingActions = []; zr.animation.on('frame', this._onframe, this); bindRenderedEvent(zr, this); // ECharts instance can be used as value. setAsPrimitive(this); } var echartsProto = ECharts.prototype; echartsProto._onframe = function () { if (this._disposed) { return; } var scheduler = this._scheduler; // Lazy update if (this[OPTION_UPDATED]) { var silent = this[OPTION_UPDATED].silent; this[IN_MAIN_PROCESS] = true; prepare(this); updateMethods.update.call(this); this[IN_MAIN_PROCESS] = false; this[OPTION_UPDATED] = false; flushPendingActions.call(this, silent); triggerUpdatedEvent.call(this, silent); } // Avoid do both lazy update and progress in one frame. else if (scheduler.unfinished) { // Stream progress. var remainTime = TEST_FRAME_REMAIN_TIME; var ecModel = this._model; var api = this._api; scheduler.unfinished = false; do { var startTime = +new Date(); scheduler.performSeriesTasks(ecModel); // Currently dataProcessorFuncs do not check threshold. scheduler.performDataProcessorTasks(ecModel); updateStreamModes(this, ecModel); // Do not update coordinate system here. Because that coord system update in // each frame is not a good user experience. So we follow the rule that // the extent of the coordinate system is determin in the first frame (the // frame is executed immedietely after task reset. // this._coordSysMgr.update(ecModel, api); // console.log('--- ec frame visual ---', remainTime); scheduler.performVisualTasks(ecModel); renderSeries(this, this._model, api, 'remain'); remainTime -= (+new Date() - startTime); } while (remainTime > 0 && scheduler.unfinished); // Call flush explicitly for trigger finished event. if (!scheduler.unfinished) { this._zr.flush(); } // Else, zr flushing be ensue within the same frame, // because zr flushing is after onframe event. } }; /** * @return {HTMLElement} */ echartsProto.getDom = function () { return this._dom; }; /** * @return {module:zrender~ZRender} */ echartsProto.getZr = function () { return this._zr; }; /** * Usage: * chart.setOption(option, notMerge, lazyUpdate); * chart.setOption(option, { * notMerge: ..., * lazyUpdate: ..., * silent: ... * }); * * @param {Object} option * @param {Object|boolean} [opts] opts or notMerge. * @param {boolean} [opts.notMerge=false] * @param {boolean} [opts.lazyUpdate=false] Useful when setOption frequently. */ echartsProto.setOption = function (option, notMerge, lazyUpdate) { if (__DEV__) { assert(!this[IN_MAIN_PROCESS], '`setOption` should not be called during main process.'); } if (this._disposed) { disposedWarning(this.id); return; } var silent; if (isObject(notMerge)) { lazyUpdate = notMerge.lazyUpdate; silent = notMerge.silent; notMerge = notMerge.notMerge; } this[IN_MAIN_PROCESS] = true; if (!this._model || notMerge) { var optionManager = new OptionManager(this._api); var theme$$1 = this._theme; var ecModel = this._model = new GlobalModel(); ecModel.scheduler = this._scheduler; ecModel.init(null, null, theme$$1, optionManager); } this._model.setOption(option, optionPreprocessorFuncs); if (lazyUpdate) { this[OPTION_UPDATED] = {silent: silent}; this[IN_MAIN_PROCESS] = false; } else { prepare(this); updateMethods.update.call(this); // Ensure zr refresh sychronously, and then pixel in canvas can be // fetched after `setOption`. this._zr.flush(); this[OPTION_UPDATED] = false; this[IN_MAIN_PROCESS] = false; flushPendingActions.call(this, silent); triggerUpdatedEvent.call(this, silent); } }; /** * @DEPRECATED */ echartsProto.setTheme = function () { console.error('ECharts#setTheme() is DEPRECATED in ECharts 3.0'); }; /** * @return {module:echarts/model/Global} */ echartsProto.getModel = function () { return this._model; }; /** * @return {Object} */ echartsProto.getOption = function () { return this._model && this._model.getOption(); }; /** * @return {number} */ echartsProto.getWidth = function () { return this._zr.getWidth(); }; /** * @return {number} */ echartsProto.getHeight = function () { return this._zr.getHeight(); }; /** * @return {number} */ echartsProto.getDevicePixelRatio = function () { return this._zr.painter.dpr || window.devicePixelRatio || 1; }; /** * Get canvas which has all thing rendered * @param {Object} opts * @param {string} [opts.backgroundColor] * @return {string} */ echartsProto.getRenderedCanvas = function (opts) { if (!env$1.canvasSupported) { return; } opts = opts || {}; opts.pixelRatio = opts.pixelRatio || 1; opts.backgroundColor = opts.backgroundColor || this._model.get('backgroundColor'); var zr = this._zr; // var list = zr.storage.getDisplayList(); // Stop animations // Never works before in init animation, so remove it. // zrUtil.each(list, function (el) { // el.stopAnimation(true); // }); return zr.painter.getRenderedCanvas(opts); }; /** * Get svg data url * @return {string} */ echartsProto.getSvgDataURL = function () { if (!env$1.svgSupported) { return; } var zr = this._zr; var list = zr.storage.getDisplayList(); // Stop animations each$1(list, function (el) { el.stopAnimation(true); }); return zr.painter.toDataURL(); }; /** * @return {string} * @param {Object} opts * @param {string} [opts.type='png'] * @param {string} [opts.pixelRatio=1] * @param {string} [opts.backgroundColor] * @param {string} [opts.excludeComponents] */ echartsProto.getDataURL = function (opts) { if (this._disposed) { disposedWarning(this.id); return; } opts = opts || {}; var excludeComponents = opts.excludeComponents; var ecModel = this._model; var excludesComponentViews = []; var self = this; each(excludeComponents, function (componentType) { ecModel.eachComponent({ mainType: componentType }, function (component) { var view = self._componentsMap[component.__viewId]; if (!view.group.ignore) { excludesComponentViews.push(view); view.group.ignore = true; } }); }); var url = this._zr.painter.getType() === 'svg' ? this.getSvgDataURL() : this.getRenderedCanvas(opts).toDataURL( 'image/' + (opts && opts.type || 'png') ); each(excludesComponentViews, function (view) { view.group.ignore = false; }); return url; }; /** * @return {string} * @param {Object} opts * @param {string} [opts.type='png'] * @param {string} [opts.pixelRatio=1] * @param {string} [opts.backgroundColor] */ echartsProto.getConnectedDataURL = function (opts) { if (this._disposed) { disposedWarning(this.id); return; } if (!env$1.canvasSupported) { return; } var isSvg = opts.type === 'svg'; var groupId = this.group; var mathMin = Math.min; var mathMax = Math.max; var MAX_NUMBER = Infinity; if (connectedGroups[groupId]) { var left = MAX_NUMBER; var top = MAX_NUMBER; var right = -MAX_NUMBER; var bottom = -MAX_NUMBER; var canvasList = []; var dpr = (opts && opts.pixelRatio) || 1; each$1(instances, function (chart, id) { if (chart.group === groupId) { var canvas = isSvg ? chart.getZr().painter.getSvgDom().innerHTML : chart.getRenderedCanvas(clone(opts)); var boundingRect = chart.getDom().getBoundingClientRect(); left = mathMin(boundingRect.left, left); top = mathMin(boundingRect.top, top); right = mathMax(boundingRect.right, right); bottom = mathMax(boundingRect.bottom, bottom); canvasList.push({ dom: canvas, left: boundingRect.left, top: boundingRect.top }); } }); left *= dpr; top *= dpr; right *= dpr; bottom *= dpr; var width = right - left; var height = bottom - top; var targetCanvas = createCanvas(); var zr = init$1(targetCanvas, { renderer: isSvg ? 'svg' : 'canvas' }); zr.resize({ width: width, height: height }); if (isSvg) { var content = ''; each(canvasList, function (item) { var x = item.left - left; var y = item.top - top; content += '' + item.dom + ''; }); zr.painter.getSvgRoot().innerHTML = content; if (opts.connectedBackgroundColor) { zr.painter.setBackgroundColor(opts.connectedBackgroundColor); } zr.refreshImmediately(); return zr.painter.toDataURL(); } else { // Background between the charts if (opts.connectedBackgroundColor) { zr.add(new Rect({ shape: { x: 0, y: 0, width: width, height: height }, style: { fill: opts.connectedBackgroundColor } })); } each(canvasList, function (item) { var img = new ZImage({ style: { x: item.left * dpr - left, y: item.top * dpr - top, image: item.dom } }); zr.add(img); }); zr.refreshImmediately(); return targetCanvas.toDataURL('image/' + (opts && opts.type || 'png')); } } else { return this.getDataURL(opts); } }; /** * Convert from logical coordinate system to pixel coordinate system. * See CoordinateSystem#convertToPixel. * @param {string|Object} finder * If string, e.g., 'geo', means {geoIndex: 0}. * If Object, could contain some of these properties below: * { * seriesIndex / seriesId / seriesName, * geoIndex / geoId, geoName, * bmapIndex / bmapId / bmapName, * xAxisIndex / xAxisId / xAxisName, * yAxisIndex / yAxisId / yAxisName, * gridIndex / gridId / gridName, * ... (can be extended) * } * @param {Array|number} value * @return {Array|number} result */ echartsProto.convertToPixel = curry(doConvertPixel, 'convertToPixel'); /** * Convert from pixel coordinate system to logical coordinate system. * See CoordinateSystem#convertFromPixel. * @param {string|Object} finder * If string, e.g., 'geo', means {geoIndex: 0}. * If Object, could contain some of these properties below: * { * seriesIndex / seriesId / seriesName, * geoIndex / geoId / geoName, * bmapIndex / bmapId / bmapName, * xAxisIndex / xAxisId / xAxisName, * yAxisIndex / yAxisId / yAxisName * gridIndex / gridId / gridName, * ... (can be extended) * } * @param {Array|number} value * @return {Array|number} result */ echartsProto.convertFromPixel = curry(doConvertPixel, 'convertFromPixel'); function doConvertPixel(methodName, finder, value) { if (this._disposed) { disposedWarning(this.id); return; } var ecModel = this._model; var coordSysList = this._coordSysMgr.getCoordinateSystems(); var result; finder = parseFinder(ecModel, finder); for (var i = 0; i < coordSysList.length; i++) { var coordSys = coordSysList[i]; if (coordSys[methodName] && (result = coordSys[methodName](ecModel, finder, value)) != null ) { return result; } } if (__DEV__) { console.warn( 'No coordinate system that supports ' + methodName + ' found by the given finder.' ); } } /** * Is the specified coordinate systems or components contain the given pixel point. * @param {string|Object} finder * If string, e.g., 'geo', means {geoIndex: 0}. * If Object, could contain some of these properties below: * { * seriesIndex / seriesId / seriesName, * geoIndex / geoId / geoName, * bmapIndex / bmapId / bmapName, * xAxisIndex / xAxisId / xAxisName, * yAxisIndex / yAxisId / yAxisName, * gridIndex / gridId / gridName, * ... (can be extended) * } * @param {Array|number} value * @return {boolean} result */ echartsProto.containPixel = function (finder, value) { if (this._disposed) { disposedWarning(this.id); return; } var ecModel = this._model; var result; finder = parseFinder(ecModel, finder); each$1(finder, function (models, key) { key.indexOf('Models') >= 0 && each$1(models, function (model) { var coordSys = model.coordinateSystem; if (coordSys && coordSys.containPoint) { result |= !!coordSys.containPoint(value); } else if (key === 'seriesModels') { var view = this._chartsMap[model.__viewId]; if (view && view.containPoint) { result |= view.containPoint(value, model); } else { if (__DEV__) { console.warn(key + ': ' + (view ? 'The found component do not support containPoint.' : 'No view mapping to the found component.' )); } } } else { if (__DEV__) { console.warn(key + ': containPoint is not supported'); } } }, this); }, this); return !!result; }; /** * Get visual from series or data. * @param {string|Object} finder * If string, e.g., 'series', means {seriesIndex: 0}. * If Object, could contain some of these properties below: * { * seriesIndex / seriesId / seriesName, * dataIndex / dataIndexInside * } * If dataIndex is not specified, series visual will be fetched, * but not data item visual. * If all of seriesIndex, seriesId, seriesName are not specified, * visual will be fetched from first series. * @param {string} visualType 'color', 'symbol', 'symbolSize' */ echartsProto.getVisual = function (finder, visualType) { var ecModel = this._model; finder = parseFinder(ecModel, finder, {defaultMainType: 'series'}); var seriesModel = finder.seriesModel; if (__DEV__) { if (!seriesModel) { console.warn('There is no specified seires model'); } } var data = seriesModel.getData(); var dataIndexInside = finder.hasOwnProperty('dataIndexInside') ? finder.dataIndexInside : finder.hasOwnProperty('dataIndex') ? data.indexOfRawIndex(finder.dataIndex) : null; return dataIndexInside != null ? data.getItemVisual(dataIndexInside, visualType) : data.getVisual(visualType); }; /** * Get view of corresponding component model * @param {module:echarts/model/Component} componentModel * @return {module:echarts/view/Component} */ echartsProto.getViewOfComponentModel = function (componentModel) { return this._componentsMap[componentModel.__viewId]; }; /** * Get view of corresponding series model * @param {module:echarts/model/Series} seriesModel * @return {module:echarts/view/Chart} */ echartsProto.getViewOfSeriesModel = function (seriesModel) { return this._chartsMap[seriesModel.__viewId]; }; var updateMethods = { prepareAndUpdate: function (payload) { prepare(this); updateMethods.update.call(this, payload); }, /** * @param {Object} payload * @private */ update: function (payload) { // console.profile && console.profile('update'); var ecModel = this._model; var api = this._api; var zr = this._zr; var coordSysMgr = this._coordSysMgr; var scheduler = this._scheduler; // update before setOption if (!ecModel) { return; } scheduler.restoreData(ecModel, payload); scheduler.performSeriesTasks(ecModel); // TODO // Save total ecModel here for undo/redo (after restoring data and before processing data). // Undo (restoration of total ecModel) can be carried out in 'action' or outside API call. // Create new coordinate system each update // In LineView may save the old coordinate system and use it to get the orignal point coordSysMgr.create(ecModel, api); scheduler.performDataProcessorTasks(ecModel, payload); // Current stream render is not supported in data process. So we can update // stream modes after data processing, where the filtered data is used to // deteming whether use progressive rendering. updateStreamModes(this, ecModel); // We update stream modes before coordinate system updated, then the modes info // can be fetched when coord sys updating (consider the barGrid extent fix). But // the drawback is the full coord info can not be fetched. Fortunately this full // coord is not requied in stream mode updater currently. coordSysMgr.update(ecModel, api); clearColorPalette(ecModel); scheduler.performVisualTasks(ecModel, payload); render(this, ecModel, api, payload); // Set background var backgroundColor = ecModel.get('backgroundColor') || 'transparent'; // In IE8 if (!env$1.canvasSupported) { var colorArr = parse(backgroundColor); backgroundColor = stringify(colorArr, 'rgb'); if (colorArr[3] === 0) { backgroundColor = 'transparent'; } } else { zr.setBackgroundColor(backgroundColor); } performPostUpdateFuncs(ecModel, api); // console.profile && console.profileEnd('update'); }, /** * @param {Object} payload * @private */ updateTransform: function (payload) { var ecModel = this._model; var ecIns = this; var api = this._api; // update before setOption if (!ecModel) { return; } // ChartView.markUpdateMethod(payload, 'updateTransform'); var componentDirtyList = []; ecModel.eachComponent(function (componentType, componentModel) { var componentView = ecIns.getViewOfComponentModel(componentModel); if (componentView && componentView.__alive) { if (componentView.updateTransform) { var result = componentView.updateTransform(componentModel, ecModel, api, payload); result && result.update && componentDirtyList.push(componentView); } else { componentDirtyList.push(componentView); } } }); var seriesDirtyMap = createHashMap(); ecModel.eachSeries(function (seriesModel) { var chartView = ecIns._chartsMap[seriesModel.__viewId]; if (chartView.updateTransform) { var result = chartView.updateTransform(seriesModel, ecModel, api, payload); result && result.update && seriesDirtyMap.set(seriesModel.uid, 1); } else { seriesDirtyMap.set(seriesModel.uid, 1); } }); clearColorPalette(ecModel); // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. // this._scheduler.performVisualTasks(ecModel, payload, 'layout', true); this._scheduler.performVisualTasks( ecModel, payload, {setDirty: true, dirtyMap: seriesDirtyMap} ); // Currently, not call render of components. Geo render cost a lot. // renderComponents(ecIns, ecModel, api, payload, componentDirtyList); renderSeries(ecIns, ecModel, api, payload, seriesDirtyMap); performPostUpdateFuncs(ecModel, this._api); }, /** * @param {Object} payload * @private */ updateView: function (payload) { var ecModel = this._model; // update before setOption if (!ecModel) { return; } Chart.markUpdateMethod(payload, 'updateView'); clearColorPalette(ecModel); // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. this._scheduler.performVisualTasks(ecModel, payload, {setDirty: true}); render(this, this._model, this._api, payload); performPostUpdateFuncs(ecModel, this._api); }, /** * @param {Object} payload * @private */ updateVisual: function (payload) { updateMethods.update.call(this, payload); // var ecModel = this._model; // // update before setOption // if (!ecModel) { // return; // } // ChartView.markUpdateMethod(payload, 'updateVisual'); // clearColorPalette(ecModel); // // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. // this._scheduler.performVisualTasks(ecModel, payload, {visualType: 'visual', setDirty: true}); // render(this, this._model, this._api, payload); // performPostUpdateFuncs(ecModel, this._api); }, /** * @param {Object} payload * @private */ updateLayout: function (payload) { updateMethods.update.call(this, payload); // var ecModel = this._model; // // update before setOption // if (!ecModel) { // return; // } // ChartView.markUpdateMethod(payload, 'updateLayout'); // // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. // // this._scheduler.performVisualTasks(ecModel, payload, 'layout', true); // this._scheduler.performVisualTasks(ecModel, payload, {setDirty: true}); // render(this, this._model, this._api, payload); // performPostUpdateFuncs(ecModel, this._api); } }; function prepare(ecIns) { var ecModel = ecIns._model; var scheduler = ecIns._scheduler; scheduler.restorePipelines(ecModel); scheduler.prepareStageTasks(); prepareView(ecIns, 'component', ecModel, scheduler); prepareView(ecIns, 'chart', ecModel, scheduler); scheduler.plan(); } /** * @private */ function updateDirectly(ecIns, method, payload, mainType, subType) { var ecModel = ecIns._model; // broadcast if (!mainType) { // FIXME // Chart will not be update directly here, except set dirty. // But there is no such scenario now. each(ecIns._componentsViews.concat(ecIns._chartsViews), callView); return; } var query = {}; query[mainType + 'Id'] = payload[mainType + 'Id']; query[mainType + 'Index'] = payload[mainType + 'Index']; query[mainType + 'Name'] = payload[mainType + 'Name']; var condition = {mainType: mainType, query: query}; subType && (condition.subType = subType); // subType may be '' by parseClassType; var excludeSeriesId = payload.excludeSeriesId; if (excludeSeriesId != null) { excludeSeriesId = createHashMap(normalizeToArray(excludeSeriesId)); } // If dispatchAction before setOption, do nothing. ecModel && ecModel.eachComponent(condition, function (model) { if (!excludeSeriesId || excludeSeriesId.get(model.id) == null) { callView(ecIns[ mainType === 'series' ? '_chartsMap' : '_componentsMap' ][model.__viewId]); } }, ecIns); function callView(view) { view && view.__alive && view[method] && view[method]( view.__model, ecModel, ecIns._api, payload ); } } /** * Resize the chart * @param {Object} opts * @param {number} [opts.width] Can be 'auto' (the same as null/undefined) * @param {number} [opts.height] Can be 'auto' (the same as null/undefined) * @param {boolean} [opts.silent=false] */ echartsProto.resize = function (opts) { if (__DEV__) { assert(!this[IN_MAIN_PROCESS], '`resize` should not be called during main process.'); } if (this._disposed) { disposedWarning(this.id); return; } this._zr.resize(opts); var ecModel = this._model; // Resize loading effect this._loadingFX && this._loadingFX.resize(); if (!ecModel) { return; } var optionChanged = ecModel.resetOption('media'); var silent = opts && opts.silent; this[IN_MAIN_PROCESS] = true; optionChanged && prepare(this); updateMethods.update.call(this); this[IN_MAIN_PROCESS] = false; flushPendingActions.call(this, silent); triggerUpdatedEvent.call(this, silent); }; function updateStreamModes(ecIns, ecModel) { var chartsMap = ecIns._chartsMap; var scheduler = ecIns._scheduler; ecModel.eachSeries(function (seriesModel) { scheduler.updateStreamModes(seriesModel, chartsMap[seriesModel.__viewId]); }); } /** * Show loading effect * @param {string} [name='default'] * @param {Object} [cfg] */ echartsProto.showLoading = function (name, cfg) { if (this._disposed) { disposedWarning(this.id); return; } if (isObject(name)) { cfg = name; name = ''; } name = name || 'default'; this.hideLoading(); if (!loadingEffects[name]) { if (__DEV__) { console.warn('Loading effects ' + name + ' not exists.'); } return; } var el = loadingEffects[name](this._api, cfg); var zr = this._zr; this._loadingFX = el; zr.add(el); }; /** * Hide loading effect */ echartsProto.hideLoading = function () { if (this._disposed) { disposedWarning(this.id); return; } this._loadingFX && this._zr.remove(this._loadingFX); this._loadingFX = null; }; /** * @param {Object} eventObj * @return {Object} */ echartsProto.makeActionFromEvent = function (eventObj) { var payload = extend({}, eventObj); payload.type = eventActionMap[eventObj.type]; return payload; }; /** * @pubilc * @param {Object} payload * @param {string} [payload.type] Action type * @param {Object|boolean} [opt] If pass boolean, means opt.silent * @param {boolean} [opt.silent=false] Whether trigger events. * @param {boolean} [opt.flush=undefined] * true: Flush immediately, and then pixel in canvas can be fetched * immediately. Caution: it might affect performance. * false: Not flush. * undefined: Auto decide whether perform flush. */ echartsProto.dispatchAction = function (payload, opt) { if (this._disposed) { disposedWarning(this.id); return; } if (!isObject(opt)) { opt = {silent: !!opt}; } if (!actions[payload.type]) { return; } // Avoid dispatch action before setOption. Especially in `connect`. if (!this._model) { return; } // May dispatchAction in rendering procedure if (this[IN_MAIN_PROCESS]) { this._pendingActions.push(payload); return; } doDispatchAction.call(this, payload, opt.silent); if (opt.flush) { this._zr.flush(true); } else if (opt.flush !== false && env$1.browser.weChat) { // In WeChat embeded browser, `requestAnimationFrame` and `setInterval` // hang when sliding page (on touch event), which cause that zr does not // refresh util user interaction finished, which is not expected. // But `dispatchAction` may be called too frequently when pan on touch // screen, which impacts performance if do not throttle them. this._throttledZrFlush(); } flushPendingActions.call(this, opt.silent); triggerUpdatedEvent.call(this, opt.silent); }; function doDispatchAction(payload, silent) { var payloadType = payload.type; var escapeConnect = payload.escapeConnect; var actionWrap = actions[payloadType]; var actionInfo = actionWrap.actionInfo; var cptType = (actionInfo.update || 'update').split(':'); var updateMethod = cptType.pop(); cptType = cptType[0] != null && parseClassType(cptType[0]); this[IN_MAIN_PROCESS] = true; var payloads = [payload]; var batched = false; // Batch action if (payload.batch) { batched = true; payloads = map(payload.batch, function (item) { item = defaults(extend({}, item), payload); item.batch = null; return item; }); } var eventObjBatch = []; var eventObj; var isHighDown = payloadType === 'highlight' || payloadType === 'downplay'; each(payloads, function (batchItem) { // Action can specify the event by return it. eventObj = actionWrap.action(batchItem, this._model, this._api); // Emit event outside eventObj = eventObj || extend({}, batchItem); // Convert type to eventType eventObj.type = actionInfo.event || eventObj.type; eventObjBatch.push(eventObj); // light update does not perform data process, layout and visual. if (isHighDown) { // method, payload, mainType, subType updateDirectly(this, updateMethod, batchItem, 'series'); } else if (cptType) { updateDirectly(this, updateMethod, batchItem, cptType.main, cptType.sub); } }, this); if (updateMethod !== 'none' && !isHighDown && !cptType) { // Still dirty if (this[OPTION_UPDATED]) { // FIXME Pass payload ? prepare(this); updateMethods.update.call(this, payload); this[OPTION_UPDATED] = false; } else { updateMethods[updateMethod].call(this, payload); } } // Follow the rule of action batch if (batched) { eventObj = { type: actionInfo.event || payloadType, escapeConnect: escapeConnect, batch: eventObjBatch }; } else { eventObj = eventObjBatch[0]; } this[IN_MAIN_PROCESS] = false; !silent && this._messageCenter.trigger(eventObj.type, eventObj); } function flushPendingActions(silent) { var pendingActions = this._pendingActions; while (pendingActions.length) { var payload = pendingActions.shift(); doDispatchAction.call(this, payload, silent); } } function triggerUpdatedEvent(silent) { !silent && this.trigger('updated'); } /** * Event `rendered` is triggered when zr * rendered. It is useful for realtime * snapshot (reflect animation). * * Event `finished` is triggered when: * (1) zrender rendering finished. * (2) initial animation finished. * (3) progressive rendering finished. * (4) no pending action. * (5) no delayed setOption needs to be processed. */ function bindRenderedEvent(zr, ecIns) { zr.on('rendered', function () { ecIns.trigger('rendered'); // The `finished` event should not be triggered repeatly, // so it should only be triggered when rendering indeed happend // in zrender. (Consider the case that dipatchAction is keep // triggering when mouse move). if ( // Although zr is dirty if initial animation is not finished // and this checking is called on frame, we also check // animation finished for robustness. zr.animation.isFinished() && !ecIns[OPTION_UPDATED] && !ecIns._scheduler.unfinished && !ecIns._pendingActions.length ) { ecIns.trigger('finished'); } }); } /** * @param {Object} params * @param {number} params.seriesIndex * @param {Array|TypedArray} params.data */ echartsProto.appendData = function (params) { if (this._disposed) { disposedWarning(this.id); return; } var seriesIndex = params.seriesIndex; var ecModel = this.getModel(); var seriesModel = ecModel.getSeriesByIndex(seriesIndex); if (__DEV__) { assert(params.data && seriesModel); } seriesModel.appendData(params); // Note: `appendData` does not support that update extent of coordinate // system, util some scenario require that. In the expected usage of // `appendData`, the initial extent of coordinate system should better // be fixed by axis `min`/`max` setting or initial data, otherwise if // the extent changed while `appendData`, the location of the painted // graphic elements have to be changed, which make the usage of // `appendData` meaningless. this._scheduler.unfinished = true; }; /** * Register event * @method */ echartsProto.on = createRegisterEventWithLowercaseName('on', false); echartsProto.off = createRegisterEventWithLowercaseName('off', false); echartsProto.one = createRegisterEventWithLowercaseName('one', false); /** * Prepare view instances of charts and components * @param {module:echarts/model/Global} ecModel * @private */ function prepareView(ecIns, type, ecModel, scheduler) { var isComponent = type === 'component'; var viewList = isComponent ? ecIns._componentsViews : ecIns._chartsViews; var viewMap = isComponent ? ecIns._componentsMap : ecIns._chartsMap; var zr = ecIns._zr; var api = ecIns._api; for (var i = 0; i < viewList.length; i++) { viewList[i].__alive = false; } isComponent ? ecModel.eachComponent(function (componentType, model) { componentType !== 'series' && doPrepare(model); }) : ecModel.eachSeries(doPrepare); function doPrepare(model) { // Consider: id same and type changed. var viewId = '_ec_' + model.id + '_' + model.type; var view = viewMap[viewId]; if (!view) { var classType = parseClassType(model.type); var Clazz = isComponent ? Component$1.getClass(classType.main, classType.sub) : Chart.getClass(classType.sub); if (__DEV__) { assert(Clazz, classType.sub + ' does not exist.'); } view = new Clazz(); view.init(ecModel, api); viewMap[viewId] = view; viewList.push(view); zr.add(view.group); } model.__viewId = view.__id = viewId; view.__alive = true; view.__model = model; view.group.__ecComponentInfo = { mainType: model.mainType, index: model.componentIndex }; !isComponent && scheduler.prepareView(view, model, ecModel, api); } for (var i = 0; i < viewList.length;) { var view = viewList[i]; if (!view.__alive) { !isComponent && view.renderTask.dispose(); zr.remove(view.group); view.dispose(ecModel, api); viewList.splice(i, 1); delete viewMap[view.__id]; view.__id = view.group.__ecComponentInfo = null; } else { i++; } } } // /** // * Encode visual infomation from data after data processing // * // * @param {module:echarts/model/Global} ecModel // * @param {object} layout // * @param {boolean} [layoutFilter] `true`: only layout, // * `false`: only not layout, // * `null`/`undefined`: all. // * @param {string} taskBaseTag // * @private // */ // function startVisualEncoding(ecIns, ecModel, api, payload, layoutFilter) { // each(visualFuncs, function (visual, index) { // var isLayout = visual.isLayout; // if (layoutFilter == null // || (layoutFilter === false && !isLayout) // || (layoutFilter === true && isLayout) // ) { // visual.func(ecModel, api, payload); // } // }); // } function clearColorPalette(ecModel) { ecModel.clearColorPalette(); ecModel.eachSeries(function (seriesModel) { seriesModel.clearColorPalette(); }); } function render(ecIns, ecModel, api, payload) { renderComponents(ecIns, ecModel, api, payload); each(ecIns._chartsViews, function (chart) { chart.__alive = false; }); renderSeries(ecIns, ecModel, api, payload); // Remove groups of unrendered charts each(ecIns._chartsViews, function (chart) { if (!chart.__alive) { chart.remove(ecModel, api); } }); } function renderComponents(ecIns, ecModel, api, payload, dirtyList) { each(dirtyList || ecIns._componentsViews, function (componentView) { var componentModel = componentView.__model; componentView.render(componentModel, ecModel, api, payload); updateZ(componentModel, componentView); }); } /** * Render each chart and component * @private */ function renderSeries(ecIns, ecModel, api, payload, dirtyMap) { // Render all charts var scheduler = ecIns._scheduler; var unfinished; ecModel.eachSeries(function (seriesModel) { var chartView = ecIns._chartsMap[seriesModel.__viewId]; chartView.__alive = true; var renderTask = chartView.renderTask; scheduler.updatePayload(renderTask, payload); if (dirtyMap && dirtyMap.get(seriesModel.uid)) { renderTask.dirty(); } unfinished |= renderTask.perform(scheduler.getPerformArgs(renderTask)); chartView.group.silent = !!seriesModel.get('silent'); updateZ(seriesModel, chartView); updateBlend(seriesModel, chartView); }); scheduler.unfinished |= unfinished; // If use hover layer updateHoverLayerStatus(ecIns, ecModel); // Add aria aria(ecIns._zr.dom, ecModel); } function performPostUpdateFuncs(ecModel, api) { each(postUpdateFuncs, function (func) { func(ecModel, api); }); } var MOUSE_EVENT_NAMES = [ 'click', 'dblclick', 'mouseover', 'mouseout', 'mousemove', 'mousedown', 'mouseup', 'globalout', 'contextmenu' ]; /** * @private */ echartsProto._initEvents = function () { each(MOUSE_EVENT_NAMES, function (eveName) { var handler = function (e) { var ecModel = this.getModel(); var el = e.target; var params; var isGlobalOut = eveName === 'globalout'; // no e.target when 'globalout'. if (isGlobalOut) { params = {}; } else if (el && el.dataIndex != null) { var dataModel = el.dataModel || ecModel.getSeriesByIndex(el.seriesIndex); params = dataModel && dataModel.getDataParams(el.dataIndex, el.dataType, el) || {}; } // If element has custom eventData of components else if (el && el.eventData) { params = extend({}, el.eventData); } // Contract: if params prepared in mouse event, // these properties must be specified: // { // componentType: string (component main type) // componentIndex: number // } // Otherwise event query can not work. if (params) { var componentType = params.componentType; var componentIndex = params.componentIndex; // Special handling for historic reason: when trigger by // markLine/markPoint/markArea, the componentType is // 'markLine'/'markPoint'/'markArea', but we should better // enable them to be queried by seriesIndex, since their // option is set in each series. if (componentType === 'markLine' || componentType === 'markPoint' || componentType === 'markArea' ) { componentType = 'series'; componentIndex = params.seriesIndex; } var model = componentType && componentIndex != null && ecModel.getComponent(componentType, componentIndex); var view = model && this[ model.mainType === 'series' ? '_chartsMap' : '_componentsMap' ][model.__viewId]; if (__DEV__) { // `event.componentType` and `event[componentTpype + 'Index']` must not // be missed, otherwise there is no way to distinguish source component. // See `dataFormat.getDataParams`. if (!isGlobalOut && !(model && view)) { console.warn('model or view can not be found by params'); } } params.event = e; params.type = eveName; this._ecEventProcessor.eventInfo = { targetEl: el, packedEvent: params, model: model, view: view }; this.trigger(eveName, params); } }; // Consider that some component (like tooltip, brush, ...) // register zr event handler, but user event handler might // do anything, such as call `setOption` or `dispatchAction`, // which probably update any of the content and probably // cause problem if it is called previous other inner handlers. handler.zrEventfulCallAtLast = true; this._zr.on(eveName, handler, this); }, this); each(eventActionMap, function (actionType, eventType) { this._messageCenter.on(eventType, function (event) { this.trigger(eventType, event); }, this); }, this); }; /** * @return {boolean} */ echartsProto.isDisposed = function () { return this._disposed; }; /** * Clear */ echartsProto.clear = function () { if (this._disposed) { disposedWarning(this.id); return; } this.setOption({ series: [] }, true); }; /** * Dispose instance */ echartsProto.dispose = function () { if (this._disposed) { disposedWarning(this.id); return; } this._disposed = true; setAttribute(this.getDom(), DOM_ATTRIBUTE_KEY, ''); var api = this._api; var ecModel = this._model; each(this._componentsViews, function (component) { component.dispose(ecModel, api); }); each(this._chartsViews, function (chart) { chart.dispose(ecModel, api); }); // Dispose after all views disposed this._zr.dispose(); delete instances[this.id]; }; mixin(ECharts, Eventful); function disposedWarning(id) { if (__DEV__) { console.warn('Instance ' + id + ' has been disposed'); } } function updateHoverLayerStatus(ecIns, ecModel) { var zr = ecIns._zr; var storage = zr.storage; var elCount = 0; storage.traverse(function (el) { elCount++; }); if (elCount > ecModel.get('hoverLayerThreshold') && !env$1.node) { ecModel.eachSeries(function (seriesModel) { if (seriesModel.preventUsingHoverLayer) { return; } var chartView = ecIns._chartsMap[seriesModel.__viewId]; if (chartView.__alive) { chartView.group.traverse(function (el) { // Don't switch back. el.useHoverLayer = true; }); } }); } } /** * Update chart progressive and blend. * @param {module:echarts/model/Series|module:echarts/model/Component} model * @param {module:echarts/view/Component|module:echarts/view/Chart} view */ function updateBlend(seriesModel, chartView) { var blendMode = seriesModel.get('blendMode') || null; if (__DEV__) { if (!env$1.canvasSupported && blendMode && blendMode !== 'source-over') { console.warn('Only canvas support blendMode'); } } chartView.group.traverse(function (el) { // FIXME marker and other components if (!el.isGroup) { // Only set if blendMode is changed. In case element is incremental and don't wan't to rerender. if (el.style.blend !== blendMode) { el.setStyle('blend', blendMode); } } if (el.eachPendingDisplayable) { el.eachPendingDisplayable(function (displayable) { displayable.setStyle('blend', blendMode); }); } }); } /** * @param {module:echarts/model/Series|module:echarts/model/Component} model * @param {module:echarts/view/Component|module:echarts/view/Chart} view */ function updateZ(model, view) { var z = model.get('z'); var zlevel = model.get('zlevel'); // Set z and zlevel view.group.traverse(function (el) { if (el.type !== 'group') { z != null && (el.z = z); zlevel != null && (el.zlevel = zlevel); } }); } function createExtensionAPI(ecInstance) { var coordSysMgr = ecInstance._coordSysMgr; return extend(new ExtensionAPI(ecInstance), { // Inject methods getCoordinateSystems: bind( coordSysMgr.getCoordinateSystems, coordSysMgr ), getComponentByElement: function (el) { while (el) { var modelInfo = el.__ecComponentInfo; if (modelInfo != null) { return ecInstance._model.getComponent(modelInfo.mainType, modelInfo.index); } el = el.parent; } } }); } /** * @class * Usage of query: * `chart.on('click', query, handler);` * The `query` can be: * + The component type query string, only `mainType` or `mainType.subType`, * like: 'xAxis', 'series', 'xAxis.category' or 'series.line'. * + The component query object, like: * `{seriesIndex: 2}`, `{seriesName: 'xx'}`, `{seriesId: 'some'}`, * `{xAxisIndex: 2}`, `{xAxisName: 'xx'}`, `{xAxisId: 'some'}`. * + The data query object, like: * `{dataIndex: 123}`, `{dataType: 'link'}`, `{name: 'some'}`. * + The other query object (cmponent customized query), like: * `{element: 'some'}` (only available in custom series). * * Caveat: If a prop in the `query` object is `null/undefined`, it is the * same as there is no such prop in the `query` object. */ function EventProcessor() { // These info required: targetEl, packedEvent, model, view this.eventInfo; } EventProcessor.prototype = { constructor: EventProcessor, normalizeQuery: function (query) { var cptQuery = {}; var dataQuery = {}; var otherQuery = {}; // `query` is `mainType` or `mainType.subType` of component. if (isString(query)) { var condCptType = parseClassType(query); // `.main` and `.sub` may be ''. cptQuery.mainType = condCptType.main || null; cptQuery.subType = condCptType.sub || null; } // `query` is an object, convert to {mainType, index, name, id}. else { // `xxxIndex`, `xxxName`, `xxxId`, `name`, `dataIndex`, `dataType` is reserved, // can not be used in `compomentModel.filterForExposedEvent`. var suffixes = ['Index', 'Name', 'Id']; var dataKeys = {name: 1, dataIndex: 1, dataType: 1}; each$1(query, function (val, key) { var reserved = false; for (var i = 0; i < suffixes.length; i++) { var propSuffix = suffixes[i]; var suffixPos = key.lastIndexOf(propSuffix); if (suffixPos > 0 && suffixPos === key.length - propSuffix.length) { var mainType = key.slice(0, suffixPos); // Consider `dataIndex`. if (mainType !== 'data') { cptQuery.mainType = mainType; cptQuery[propSuffix.toLowerCase()] = val; reserved = true; } } } if (dataKeys.hasOwnProperty(key)) { dataQuery[key] = val; reserved = true; } if (!reserved) { otherQuery[key] = val; } }); } return { cptQuery: cptQuery, dataQuery: dataQuery, otherQuery: otherQuery }; }, filter: function (eventType, query, args) { // They should be assigned before each trigger call. var eventInfo = this.eventInfo; if (!eventInfo) { return true; } var targetEl = eventInfo.targetEl; var packedEvent = eventInfo.packedEvent; var model = eventInfo.model; var view = eventInfo.view; // For event like 'globalout'. if (!model || !view) { return true; } var cptQuery = query.cptQuery; var dataQuery = query.dataQuery; return check(cptQuery, model, 'mainType') && check(cptQuery, model, 'subType') && check(cptQuery, model, 'index', 'componentIndex') && check(cptQuery, model, 'name') && check(cptQuery, model, 'id') && check(dataQuery, packedEvent, 'name') && check(dataQuery, packedEvent, 'dataIndex') && check(dataQuery, packedEvent, 'dataType') && (!view.filterForExposedEvent || view.filterForExposedEvent( eventType, query.otherQuery, targetEl, packedEvent )); function check(query, host, prop, propOnHost) { return query[prop] == null || host[propOnHost || prop] === query[prop]; } }, afterTrigger: function () { // Make sure the eventInfo wont be used in next trigger. this.eventInfo = null; } }; /** * @type {Object} key: actionType. * @inner */ var actions = {}; /** * Map eventType to actionType * @type {Object} */ var eventActionMap = {}; /** * Data processor functions of each stage * @type {Array.>} * @inner */ var dataProcessorFuncs = []; /** * @type {Array.} * @inner */ var optionPreprocessorFuncs = []; /** * @type {Array.} * @inner */ var postUpdateFuncs = []; /** * Visual encoding functions of each stage * @type {Array.>} */ var visualFuncs = []; /** * Theme storage * @type {Object.} */ var themeStorage = {}; /** * Loading effects */ var loadingEffects = {}; var instances = {}; var connectedGroups = {}; var idBase = new Date() - 0; var groupIdBase = new Date() - 0; var DOM_ATTRIBUTE_KEY = '_echarts_instance_'; function enableConnect(chart) { var STATUS_PENDING = 0; var STATUS_UPDATING = 1; var STATUS_UPDATED = 2; var STATUS_KEY = '__connectUpdateStatus'; function updateConnectedChartsStatus(charts, status) { for (var i = 0; i < charts.length; i++) { var otherChart = charts[i]; otherChart[STATUS_KEY] = status; } } each(eventActionMap, function (actionType, eventType) { chart._messageCenter.on(eventType, function (event) { if (connectedGroups[chart.group] && chart[STATUS_KEY] !== STATUS_PENDING) { if (event && event.escapeConnect) { return; } var action = chart.makeActionFromEvent(event); var otherCharts = []; each(instances, function (otherChart) { if (otherChart !== chart && otherChart.group === chart.group) { otherCharts.push(otherChart); } }); updateConnectedChartsStatus(otherCharts, STATUS_PENDING); each(otherCharts, function (otherChart) { if (otherChart[STATUS_KEY] !== STATUS_UPDATING) { otherChart.dispatchAction(action); } }); updateConnectedChartsStatus(otherCharts, STATUS_UPDATED); } }); }); } /** * @param {HTMLElement} dom * @param {Object} [theme] * @param {Object} opts * @param {number} [opts.devicePixelRatio] Use window.devicePixelRatio by default * @param {string} [opts.renderer] Can choose 'canvas' or 'svg' to render the chart. * @param {number} [opts.width] Use clientWidth of the input `dom` by default. * Can be 'auto' (the same as null/undefined) * @param {number} [opts.height] Use clientHeight of the input `dom` by default. * Can be 'auto' (the same as null/undefined) */ function init(dom, theme$$1, opts) { if (__DEV__) { // Check version if ((version$1.replace('.', '') - 0) < (dependencies.zrender.replace('.', '') - 0)) { throw new Error( 'zrender/src ' + version$1 + ' is too old for ECharts ' + version + '. Current version need ZRender ' + dependencies.zrender + '+' ); } if (!dom) { throw new Error('Initialize failed: invalid dom.'); } } var existInstance = getInstanceByDom(dom); if (existInstance) { if (__DEV__) { console.warn('There is a chart instance already initialized on the dom.'); } return existInstance; } if (__DEV__) { if (isDom(dom) && dom.nodeName.toUpperCase() !== 'CANVAS' && ( (!dom.clientWidth && (!opts || opts.width == null)) || (!dom.clientHeight && (!opts || opts.height == null)) ) ) { console.warn('Can\'t get DOM width or height. Please check ' + 'dom.clientWidth and dom.clientHeight. They should not be 0.' + 'For example, you may need to call this in the callback ' + 'of window.onload.'); } } var chart = new ECharts(dom, theme$$1, opts); chart.id = 'ec_' + idBase++; instances[chart.id] = chart; setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id); enableConnect(chart); return chart; } /** * @return {string|Array.} groupId */ function connect(groupId) { // Is array of charts if (isArray(groupId)) { var charts = groupId; groupId = null; // If any chart has group each(charts, function (chart) { if (chart.group != null) { groupId = chart.group; } }); groupId = groupId || ('g_' + groupIdBase++); each(charts, function (chart) { chart.group = groupId; }); } connectedGroups[groupId] = true; return groupId; } /** * @DEPRECATED * @return {string} groupId */ function disConnect(groupId) { connectedGroups[groupId] = false; } /** * @return {string} groupId */ var disconnect = disConnect; /** * Dispose a chart instance * @param {module:echarts~ECharts|HTMLDomElement|string} chart */ function dispose(chart) { if (typeof chart === 'string') { chart = instances[chart]; } else if (!(chart instanceof ECharts)) { // Try to treat as dom chart = getInstanceByDom(chart); } if ((chart instanceof ECharts) && !chart.isDisposed()) { chart.dispose(); } } /** * @param {HTMLElement} dom * @return {echarts~ECharts} */ function getInstanceByDom(dom) { return instances[getAttribute(dom, DOM_ATTRIBUTE_KEY)]; } /** * @param {string} key * @return {echarts~ECharts} */ function getInstanceById(key) { return instances[key]; } /** * Register theme */ function registerTheme(name, theme$$1) { themeStorage[name] = theme$$1; } /** * Register option preprocessor * @param {Function} preprocessorFunc */ function registerPreprocessor(preprocessorFunc) { optionPreprocessorFuncs.push(preprocessorFunc); } /** * @param {number} [priority=1000] * @param {Object|Function} processor */ function registerProcessor(priority, processor) { normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_FILTER); } /** * Register postUpdater * @param {Function} postUpdateFunc */ function registerPostUpdate(postUpdateFunc) { postUpdateFuncs.push(postUpdateFunc); } /** * Usage: * registerAction('someAction', 'someEvent', function () { ... }); * registerAction('someAction', function () { ... }); * registerAction( * {type: 'someAction', event: 'someEvent', update: 'updateView'}, * function () { ... } * ); * * @param {(string|Object)} actionInfo * @param {string} actionInfo.type * @param {string} [actionInfo.event] * @param {string} [actionInfo.update] * @param {string} [eventName] * @param {Function} action */ function registerAction(actionInfo, eventName, action) { if (typeof eventName === 'function') { action = eventName; eventName = ''; } var actionType = isObject(actionInfo) ? actionInfo.type : ([actionInfo, actionInfo = { event: eventName }][0]); // Event name is all lowercase actionInfo.event = (actionInfo.event || actionType).toLowerCase(); eventName = actionInfo.event; // Validate action type and event name. assert(ACTION_REG.test(actionType) && ACTION_REG.test(eventName)); if (!actions[actionType]) { actions[actionType] = {action: action, actionInfo: actionInfo}; } eventActionMap[eventName] = actionType; } /** * @param {string} type * @param {*} CoordinateSystem */ function registerCoordinateSystem(type, CoordinateSystem$$1) { CoordinateSystemManager.register(type, CoordinateSystem$$1); } /** * Get dimensions of specified coordinate system. * @param {string} type * @return {Array.} */ function getCoordinateSystemDimensions(type) { var coordSysCreator = CoordinateSystemManager.get(type); if (coordSysCreator) { return coordSysCreator.getDimensionsInfo ? coordSysCreator.getDimensionsInfo() : coordSysCreator.dimensions.slice(); } } /** * Layout is a special stage of visual encoding * Most visual encoding like color are common for different chart * But each chart has it's own layout algorithm * * @param {number} [priority=1000] * @param {Function} layoutTask */ function registerLayout(priority, layoutTask) { normalizeRegister(visualFuncs, priority, layoutTask, PRIORITY_VISUAL_LAYOUT, 'layout'); } /** * @param {number} [priority=3000] * @param {module:echarts/stream/Task} visualTask */ function registerVisual(priority, visualTask) { normalizeRegister(visualFuncs, priority, visualTask, PRIORITY_VISUAL_CHART, 'visual'); } /** * @param {Object|Function} fn: {seriesType, createOnAllSeries, performRawSeries, reset} */ function normalizeRegister(targetList, priority, fn, defaultPriority, visualType) { if (isFunction(priority) || isObject(priority)) { fn = priority; priority = defaultPriority; } if (__DEV__) { if (isNaN(priority) || priority == null) { throw new Error('Illegal priority'); } // Check duplicate each(targetList, function (wrap) { assert(wrap.__raw !== fn); }); } var stageHandler = Scheduler.wrapStageHandler(fn, visualType); stageHandler.__prio = priority; stageHandler.__raw = fn; targetList.push(stageHandler); return stageHandler; } /** * @param {string} name */ function registerLoading(name, loadingFx) { loadingEffects[name] = loadingFx; } /** * @param {Object} opts * @param {string} [superClass] */ function extendComponentModel(opts/*, superClass*/) { // var Clazz = ComponentModel; // if (superClass) { // var classType = parseClassType(superClass); // Clazz = ComponentModel.getClass(classType.main, classType.sub, true); // } return ComponentModel.extend(opts); } /** * @param {Object} opts * @param {string} [superClass] */ function extendComponentView(opts/*, superClass*/) { // var Clazz = ComponentView; // if (superClass) { // var classType = parseClassType(superClass); // Clazz = ComponentView.getClass(classType.main, classType.sub, true); // } return Component$1.extend(opts); } /** * @param {Object} opts * @param {string} [superClass] */ function extendSeriesModel(opts/*, superClass*/) { // var Clazz = SeriesModel; // if (superClass) { // superClass = 'series.' + superClass.replace('series.', ''); // var classType = parseClassType(superClass); // Clazz = ComponentModel.getClass(classType.main, classType.sub, true); // } return SeriesModel.extend(opts); } /** * @param {Object} opts * @param {string} [superClass] */ function extendChartView(opts/*, superClass*/) { // var Clazz = ChartView; // if (superClass) { // superClass = superClass.replace('series.', ''); // var classType = parseClassType(superClass); // Clazz = ChartView.getClass(classType.main, true); // } return Chart.extend(opts); } /** * ZRender need a canvas context to do measureText. * But in node environment canvas may be created by node-canvas. * So we need to specify how to create a canvas instead of using document.createElement('canvas') * * Be careful of using it in the browser. * * @param {Function} creator * @example * var Canvas = require('canvas'); * var echarts = require('echarts'); * echarts.setCanvasCreator(function () { * // Small size is enough. * return new Canvas(32, 32); * }); */ function setCanvasCreator(creator) { $override('createCanvas', creator); } /** * @param {string} mapName * @param {Array.|Object|string} geoJson * @param {Object} [specialAreas] * * @example GeoJSON * $.get('USA.json', function (geoJson) { * echarts.registerMap('USA', geoJson); * // Or * echarts.registerMap('USA', { * geoJson: geoJson, * specialAreas: {} * }) * }); * * $.get('airport.svg', function (svg) { * echarts.registerMap('airport', { * svg: svg * } * }); * * echarts.registerMap('eu', [ * {svg: eu-topographic.svg}, * {geoJSON: eu.json} * ]) */ function registerMap(mapName, geoJson, specialAreas) { mapDataStorage.registerMap(mapName, geoJson, specialAreas); } /** * @param {string} mapName * @return {Object} */ function getMap(mapName) { // For backward compatibility, only return the first one. var records = mapDataStorage.retrieveMap(mapName); return records && records[0] && { geoJson: records[0].geoJSON, specialAreas: records[0].specialAreas }; } registerVisual(PRIORITY_VISUAL_GLOBAL, seriesColor); registerPreprocessor(backwardCompat); registerProcessor(PRIORITY_PROCESSOR_DATASTACK, dataStack); registerLoading('default', loadingDefault); // Default actions registerAction({ type: 'highlight', event: 'highlight', update: 'highlight' }, noop); registerAction({ type: 'downplay', event: 'downplay', update: 'downplay' }, noop); // Default theme registerTheme('light', lightTheme); registerTheme('dark', theme); // For backward compatibility, where the namespace `dataTool` will // be mounted on `echarts` is the extension `dataTool` is imported. var dataTool = {}; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function defaultKeyGetter(item) { return item; } /** * @param {Array} oldArr * @param {Array} newArr * @param {Function} oldKeyGetter * @param {Function} newKeyGetter * @param {Object} [context] Can be visited by this.context in callback. */ function DataDiffer(oldArr, newArr, oldKeyGetter, newKeyGetter, context) { this._old = oldArr; this._new = newArr; this._oldKeyGetter = oldKeyGetter || defaultKeyGetter; this._newKeyGetter = newKeyGetter || defaultKeyGetter; this.context = context; } DataDiffer.prototype = { constructor: DataDiffer, /** * Callback function when add a data */ add: function (func) { this._add = func; return this; }, /** * Callback function when update a data */ update: function (func) { this._update = func; return this; }, /** * Callback function when remove a data */ remove: function (func) { this._remove = func; return this; }, execute: function () { var oldArr = this._old; var newArr = this._new; var oldDataIndexMap = {}; var newDataIndexMap = {}; var oldDataKeyArr = []; var newDataKeyArr = []; var i; initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter', this); initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter', this); for (i = 0; i < oldArr.length; i++) { var key = oldDataKeyArr[i]; var idx = newDataIndexMap[key]; // idx can never be empty array here. see 'set null' logic below. if (idx != null) { // Consider there is duplicate key (for example, use dataItem.name as key). // We should make sure every item in newArr and oldArr can be visited. var len = idx.length; if (len) { len === 1 && (newDataIndexMap[key] = null); idx = idx.shift(); } else { newDataIndexMap[key] = null; } this._update && this._update(idx, i); } else { this._remove && this._remove(i); } } for (var i = 0; i < newDataKeyArr.length; i++) { var key = newDataKeyArr[i]; if (newDataIndexMap.hasOwnProperty(key)) { var idx = newDataIndexMap[key]; if (idx == null) { continue; } // idx can never be empty array here. see 'set null' logic above. if (!idx.length) { this._add && this._add(idx); } else { for (var j = 0, len = idx.length; j < len; j++) { this._add && this._add(idx[j]); } } } } } }; function initIndexMap(arr, map, keyArr, keyGetterName, dataDiffer) { for (var i = 0; i < arr.length; i++) { // Add prefix to avoid conflict with Object.prototype. var key = '_ec_' + dataDiffer[keyGetterName](arr[i], i); var existence = map[key]; if (existence == null) { keyArr.push(key); map[key] = i; } else { if (!existence.length) { map[key] = existence = [existence]; } existence.push(i); } } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var OTHER_DIMENSIONS = createHashMap([ 'tooltip', 'label', 'itemName', 'itemId', 'seriesName' ]); function summarizeDimensions(data) { var summary = {}; var encode = summary.encode = {}; var notExtraCoordDimMap = createHashMap(); var defaultedLabel = []; var defaultedTooltip = []; // See the comment of `List.js#userOutput`. var userOutput = summary.userOutput = { dimensionNames: data.dimensions.slice(), encode: {} }; each$1(data.dimensions, function (dimName) { var dimItem = data.getDimensionInfo(dimName); var coordDim = dimItem.coordDim; if (coordDim) { if (__DEV__) { assert$1(OTHER_DIMENSIONS.get(coordDim) == null); } var coordDimIndex = dimItem.coordDimIndex; getOrCreateEncodeArr(encode, coordDim)[coordDimIndex] = dimName; if (!dimItem.isExtraCoord) { notExtraCoordDimMap.set(coordDim, 1); // Use the last coord dim (and label friendly) as default label, // because when dataset is used, it is hard to guess which dimension // can be value dimension. If both show x, y on label is not look good, // and conventionally y axis is focused more. if (mayLabelDimType(dimItem.type)) { defaultedLabel[0] = dimName; } // User output encode do not contain generated coords. // And it only has index. User can use index to retrieve value from the raw item array. getOrCreateEncodeArr(userOutput.encode, coordDim)[coordDimIndex] = dimItem.index; } if (dimItem.defaultTooltip) { defaultedTooltip.push(dimName); } } OTHER_DIMENSIONS.each(function (v, otherDim) { var encodeArr = getOrCreateEncodeArr(encode, otherDim); var dimIndex = dimItem.otherDims[otherDim]; if (dimIndex != null && dimIndex !== false) { encodeArr[dimIndex] = dimItem.name; } }); }); var dataDimsOnCoord = []; var encodeFirstDimNotExtra = {}; notExtraCoordDimMap.each(function (v, coordDim) { var dimArr = encode[coordDim]; // ??? FIXME extra coord should not be set in dataDimsOnCoord. // But should fix the case that radar axes: simplify the logic // of `completeDimension`, remove `extraPrefix`. encodeFirstDimNotExtra[coordDim] = dimArr[0]; // Not necessary to remove duplicate, because a data // dim canot on more than one coordDim. dataDimsOnCoord = dataDimsOnCoord.concat(dimArr); }); summary.dataDimsOnCoord = dataDimsOnCoord; summary.encodeFirstDimNotExtra = encodeFirstDimNotExtra; var encodeLabel = encode.label; // FIXME `encode.label` is not recommanded, because formatter can not be set // in this way. Use label.formatter instead. May be remove this approach someday. if (encodeLabel && encodeLabel.length) { defaultedLabel = encodeLabel.slice(); } var encodeTooltip = encode.tooltip; if (encodeTooltip && encodeTooltip.length) { defaultedTooltip = encodeTooltip.slice(); } else if (!defaultedTooltip.length) { defaultedTooltip = defaultedLabel.slice(); } encode.defaultedLabel = defaultedLabel; encode.defaultedTooltip = defaultedTooltip; return summary; } function getOrCreateEncodeArr(encode, dim) { if (!encode.hasOwnProperty(dim)) { encode[dim] = []; } return encode[dim]; } function getDimensionTypeByAxis(axisType) { return axisType === 'category' ? 'ordinal' : axisType === 'time' ? 'time' : 'float'; } function mayLabelDimType(dimType) { // In most cases, ordinal and time do not suitable for label. // Ordinal info can be displayed on axis. Time is too long. return !(dimType === 'ordinal' || dimType === 'time'); } // function findTheLastDimMayLabel(data) { // // Get last value dim // var dimensions = data.dimensions.slice(); // var valueType; // var valueDim; // while (dimensions.length && ( // valueDim = dimensions.pop(), // valueType = data.getDimensionInfo(valueDim).type, // valueType === 'ordinal' || valueType === 'time' // )) {} // jshint ignore:line // return valueDim; // } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @class * @param {Object|DataDimensionInfo} [opt] All of the fields will be shallow copied. */ function DataDimensionInfo(opt) { if (opt != null) { extend(this, opt); } /** * Dimension name. * Mandatory. * @type {string} */ // this.name; /** * The origin name in dimsDef, see source helper. * If displayName given, the tooltip will displayed vertically. * Optional. * @type {string} */ // this.displayName; /** * Which coordSys dimension this dimension mapped to. * A `coordDim` can be a "coordSysDim" that the coordSys required * (for example, an item in `coordSysDims` of `model/referHelper#CoordSysInfo`), * or an generated "extra coord name" if does not mapped to any "coordSysDim" * (That is determined by whether `isExtraCoord` is `true`). * Mandatory. * @type {string} */ // this.coordDim; /** * The index of this dimension in `series.encode[coordDim]`. * Mandatory. * @type {number} */ // this.coordDimIndex; /** * Dimension type. The enumerable values are the key of * `dataCtors` of `data/List`. * Optional. * @type {string} */ // this.type; /** * This index of this dimension info in `data/List#_dimensionInfos`. * Mandatory after added to `data/List`. * @type {number} */ // this.index; /** * The format of `otherDims` is: * ```js * { * tooltip: number optional, * label: number optional, * itemName: number optional, * seriesName: number optional, * } * ``` * * A `series.encode` can specified these fields: * ```js * encode: { * // "3, 1, 5" is the index of data dimension. * tooltip: [3, 1, 5], * label: [0, 3], * ... * } * ``` * `otherDims` is the parse result of the `series.encode` above, like: * ```js * // Suppose the index of this data dimension is `3`. * this.otherDims = { * // `3` is at the index `0` of the `encode.tooltip` * tooltip: 0, * // `3` is at the index `1` of the `encode.tooltip` * label: 1 * }; * ``` * * This prop should never be `null`/`undefined` after initialized. * @type {Object} */ this.otherDims = {}; /** * Be `true` if this dimension is not mapped to any "coordSysDim" that the * "coordSys" required. * Mandatory. * @type {boolean} */ // this.isExtraCoord; /** * @type {module:data/OrdinalMeta} */ // this.ordinalMeta; /** * Whether to create inverted indices. * @type {boolean} */ // this.createInvertedIndices; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global Float64Array, Int32Array, Uint32Array, Uint16Array */ /** * List for data storage * @module echarts/data/List */ var isObject$4 = isObject$1; var UNDEFINED = 'undefined'; var INDEX_NOT_FOUND = -1; // Use prefix to avoid index to be the same as otherIdList[idx], // which will cause weird udpate animation. var ID_PREFIX = 'e\0\0'; var dataCtors = { 'float': typeof Float64Array === UNDEFINED ? Array : Float64Array, 'int': typeof Int32Array === UNDEFINED ? Array : Int32Array, // Ordinal data type can be string or int 'ordinal': Array, 'number': Array, 'time': Array }; // Caution: MUST not use `new CtorUint32Array(arr, 0, len)`, because the Ctor of array is // different from the Ctor of typed array. var CtorUint32Array = typeof Uint32Array === UNDEFINED ? Array : Uint32Array; var CtorInt32Array = typeof Int32Array === UNDEFINED ? Array : Int32Array; var CtorUint16Array = typeof Uint16Array === UNDEFINED ? Array : Uint16Array; function getIndicesCtor(list) { // The possible max value in this._indicies is always this._rawCount despite of filtering. return list._rawCount > 65535 ? CtorUint32Array : CtorUint16Array; } function cloneChunk(originalChunk) { var Ctor = originalChunk.constructor; // Only shallow clone is enough when Array. return Ctor === Array ? originalChunk.slice() : new Ctor(originalChunk); } var TRANSFERABLE_PROPERTIES = [ 'hasItemOption', '_nameList', '_idList', '_invertedIndicesMap', '_rawData', '_chunkSize', '_chunkCount', '_dimValueGetter', '_count', '_rawCount', '_nameDimIdx', '_idDimIdx' ]; var CLONE_PROPERTIES = [ '_extent', '_approximateExtent', '_rawExtent' ]; function transferProperties(target, source) { each$1(TRANSFERABLE_PROPERTIES.concat(source.__wrappedMethods || []), function (propName) { if (source.hasOwnProperty(propName)) { target[propName] = source[propName]; } }); target.__wrappedMethods = source.__wrappedMethods; each$1(CLONE_PROPERTIES, function (propName) { target[propName] = clone(source[propName]); }); target._calculationInfo = extend(source._calculationInfo); } /** * @constructor * @alias module:echarts/data/List * * @param {Array.} dimensions * For example, ['someDimName', {name: 'someDimName', type: 'someDimType'}, ...]. * Dimensions should be concrete names like x, y, z, lng, lat, angle, radius * @param {module:echarts/model/Model} hostModel */ var List = function (dimensions, hostModel) { dimensions = dimensions || ['x', 'y']; var dimensionInfos = {}; var dimensionNames = []; var invertedIndicesMap = {}; for (var i = 0; i < dimensions.length; i++) { // Use the original dimensions[i], where other flag props may exists. var dimensionInfo = dimensions[i]; if (isString(dimensionInfo)) { dimensionInfo = new DataDimensionInfo({name: dimensionInfo}); } else if (!(dimensionInfo instanceof DataDimensionInfo)) { dimensionInfo = new DataDimensionInfo(dimensionInfo); } var dimensionName = dimensionInfo.name; dimensionInfo.type = dimensionInfo.type || 'float'; if (!dimensionInfo.coordDim) { dimensionInfo.coordDim = dimensionName; dimensionInfo.coordDimIndex = 0; } dimensionInfo.otherDims = dimensionInfo.otherDims || {}; dimensionNames.push(dimensionName); dimensionInfos[dimensionName] = dimensionInfo; dimensionInfo.index = i; if (dimensionInfo.createInvertedIndices) { invertedIndicesMap[dimensionName] = []; } } /** * @readOnly * @type {Array.} */ this.dimensions = dimensionNames; /** * Infomation of each data dimension, like data type. * @type {Object} */ this._dimensionInfos = dimensionInfos; /** * @type {module:echarts/model/Model} */ this.hostModel = hostModel; /** * @type {module:echarts/model/Model} */ this.dataType; /** * Indices stores the indices of data subset after filtered. * This data subset will be used in chart. * @type {Array.} * @readOnly */ this._indices = null; this._count = 0; this._rawCount = 0; /** * Data storage * @type {Object.>} * @private */ this._storage = {}; /** * @type {Array.} */ this._nameList = []; /** * @type {Array.} */ this._idList = []; /** * Models of data option is stored sparse for optimizing memory cost * @type {Array.} * @private */ this._optionModels = []; /** * Global visual properties after visual coding * @type {Object} * @private */ this._visual = {}; /** * Globel layout properties. * @type {Object} * @private */ this._layout = {}; /** * Item visual properties after visual coding * @type {Array.} * @private */ this._itemVisuals = []; /** * Key: visual type, Value: boolean * @type {Object} * @readOnly */ this.hasItemVisual = {}; /** * Item layout properties after layout * @type {Array.} * @private */ this._itemLayouts = []; /** * Graphic elemnents * @type {Array.} * @private */ this._graphicEls = []; /** * Max size of each chunk. * @type {number} * @private */ this._chunkSize = 1e5; /** * @type {number} * @private */ this._chunkCount = 0; /** * @type {Array.} * @private */ this._rawData; /** * Raw extent will not be cloned, but only transfered. * It will not be calculated util needed. * key: dim, * value: {end: number, extent: Array.} * @type {Object} * @private */ this._rawExtent = {}; /** * @type {Object} * @private */ this._extent = {}; /** * key: dim * value: extent * @type {Object} * @private */ this._approximateExtent = {}; /** * Cache summary info for fast visit. See "dimensionHelper". * @type {Object} * @private */ this._dimensionsSummary = summarizeDimensions(this); /** * @type {Object.} * @private */ this._invertedIndicesMap = invertedIndicesMap; /** * @type {Object} * @private */ this._calculationInfo = {}; /** * User output info of this data. * DO NOT use it in other places! * * When preparing user params for user callbacks, we have * to clone these inner data structures to prevent users * from modifying them to effect built-in logic. And for * performance consideration we make this `userOutput` to * avoid clone them too many times. * * @type {Object} * @readOnly */ this.userOutput = this._dimensionsSummary.userOutput; }; var listProto = List.prototype; listProto.type = 'list'; /** * If each data item has it's own option * @type {boolean} */ listProto.hasItemOption = true; /** * The meanings of the input parameter `dim`: * * + If dim is a number (e.g., `1`), it means the index of the dimension. * For example, `getDimension(0)` will return 'x' or 'lng' or 'radius'. * + If dim is a number-like string (e.g., `"1"`): * + If there is the same concrete dim name defined in `this.dimensions`, it means that concrete name. * + If not, it will be converted to a number, which means the index of the dimension. * (why? because of the backward compatbility. We have been tolerating number-like string in * dimension setting, although now it seems that it is not a good idea.) * For example, `visualMap[i].dimension: "1"` is the same meaning as `visualMap[i].dimension: 1`, * if no dimension name is defined as `"1"`. * + If dim is a not-number-like string, it means the concrete dim name. * For example, it can be be default name `"x"`, `"y"`, `"z"`, `"lng"`, `"lat"`, `"angle"`, `"radius"`, * or customized in `dimensions` property of option like `"age"`. * * Get dimension name * @param {string|number} dim See above. * @return {string} Concrete dim name. */ listProto.getDimension = function (dim) { if (typeof dim === 'number' // If being a number-like string but not being defined a dimension name. || (!isNaN(dim) && !this._dimensionInfos.hasOwnProperty(dim)) ) { dim = this.dimensions[dim]; } return dim; }; /** * Get type and calculation info of particular dimension * @param {string|number} dim * Dimension can be concrete names like x, y, z, lng, lat, angle, radius * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' */ listProto.getDimensionInfo = function (dim) { // Do not clone, because there may be categories in dimInfo. return this._dimensionInfos[this.getDimension(dim)]; }; /** * @return {Array.} concrete dimension name list on coord. */ listProto.getDimensionsOnCoord = function () { return this._dimensionsSummary.dataDimsOnCoord.slice(); }; /** * @param {string} coordDim * @param {number} [idx] A coordDim may map to more than one data dim. * If idx is `true`, return a array of all mapped dims. * If idx is not specified, return the first dim not extra. * @return {string|Array.} concrete data dim. * If idx is number, and not found, return null/undefined. * If idx is `true`, and not found, return empty array (always return array). */ listProto.mapDimension = function (coordDim, idx) { var dimensionsSummary = this._dimensionsSummary; if (idx == null) { return dimensionsSummary.encodeFirstDimNotExtra[coordDim]; } var dims = dimensionsSummary.encode[coordDim]; return idx === true // always return array if idx is `true` ? (dims || []).slice() : (dims && dims[idx]); }; /** * Initialize from data * @param {Array.} data source or data or data provider. * @param {Array.} [nameLIst] The name of a datum is used on data diff and * default label/tooltip. * A name can be specified in encode.itemName, * or dataItem.name (only for series option data), * or provided in nameList from outside. * @param {Function} [dimValueGetter] (dataItem, dimName, dataIndex, dimIndex) => number */ listProto.initData = function (data, nameList, dimValueGetter) { var notProvider = Source.isInstance(data) || isArrayLike(data); if (notProvider) { data = new DefaultDataProvider(data, this.dimensions.length); } if (__DEV__) { if (!notProvider && (typeof data.getItem !== 'function' || typeof data.count !== 'function')) { throw new Error('Inavlid data provider.'); } } this._rawData = data; // Clear this._storage = {}; this._indices = null; this._nameList = nameList || []; this._idList = []; this._nameRepeatCount = {}; if (!dimValueGetter) { this.hasItemOption = false; } /** * @readOnly */ this.defaultDimValueGetter = defaultDimValueGetters[ this._rawData.getSource().sourceFormat ]; // Default dim value getter this._dimValueGetter = dimValueGetter = dimValueGetter || this.defaultDimValueGetter; this._dimValueGetterArrayRows = defaultDimValueGetters.arrayRows; // Reset raw extent. this._rawExtent = {}; this._initDataFromProvider(0, data.count()); // If data has no item option. if (data.pure) { this.hasItemOption = false; } }; listProto.getProvider = function () { return this._rawData; }; /** * Caution: Can be only called on raw data (before `this._indices` created). */ listProto.appendData = function (data) { if (__DEV__) { assert$1(!this._indices, 'appendData can only be called on raw data.'); } var rawData = this._rawData; var start = this.count(); rawData.appendData(data); var end = rawData.count(); if (!rawData.persistent) { end += start; } this._initDataFromProvider(start, end); }; /** * Caution: Can be only called on raw data (before `this._indices` created). * This method does not modify `rawData` (`dataProvider`), but only * add values to storage. * * The final count will be increased by `Math.max(values.length, names.length)`. * * @param {Array.>} values That is the SourceType: 'arrayRows', like * [ * [12, 33, 44], * [NaN, 43, 1], * ['-', 'asdf', 0] * ] * Each item is exaclty cooresponding to a dimension. * @param {Array.} [names] */ listProto.appendValues = function (values, names) { var chunkSize = this._chunkSize; var storage = this._storage; var dimensions = this.dimensions; var dimLen = dimensions.length; var rawExtent = this._rawExtent; var start = this.count(); var end = start + Math.max(values.length, names ? names.length : 0); var originalChunkCount = this._chunkCount; for (var i = 0; i < dimLen; i++) { var dim = dimensions[i]; if (!rawExtent[dim]) { rawExtent[dim] = getInitialExtent(); } if (!storage[dim]) { storage[dim] = []; } prepareChunks(storage, this._dimensionInfos[dim], chunkSize, originalChunkCount, end); this._chunkCount = storage[dim].length; } var emptyDataItem = new Array(dimLen); for (var idx = start; idx < end; idx++) { var sourceIdx = idx - start; var chunkIndex = Math.floor(idx / chunkSize); var chunkOffset = idx % chunkSize; // Store the data by dimensions for (var k = 0; k < dimLen; k++) { var dim = dimensions[k]; var val = this._dimValueGetterArrayRows( values[sourceIdx] || emptyDataItem, dim, sourceIdx, k ); storage[dim][chunkIndex][chunkOffset] = val; var dimRawExtent = rawExtent[dim]; val < dimRawExtent[0] && (dimRawExtent[0] = val); val > dimRawExtent[1] && (dimRawExtent[1] = val); } if (names) { this._nameList[idx] = names[sourceIdx]; } } this._rawCount = this._count = end; // Reset data extent this._extent = {}; prepareInvertedIndex(this); }; listProto._initDataFromProvider = function (start, end) { // Optimize. if (start >= end) { return; } var chunkSize = this._chunkSize; var rawData = this._rawData; var storage = this._storage; var dimensions = this.dimensions; var dimLen = dimensions.length; var dimensionInfoMap = this._dimensionInfos; var nameList = this._nameList; var idList = this._idList; var rawExtent = this._rawExtent; var nameRepeatCount = this._nameRepeatCount = {}; var nameDimIdx; var originalChunkCount = this._chunkCount; for (var i = 0; i < dimLen; i++) { var dim = dimensions[i]; if (!rawExtent[dim]) { rawExtent[dim] = getInitialExtent(); } var dimInfo = dimensionInfoMap[dim]; if (dimInfo.otherDims.itemName === 0) { nameDimIdx = this._nameDimIdx = i; } if (dimInfo.otherDims.itemId === 0) { this._idDimIdx = i; } if (!storage[dim]) { storage[dim] = []; } prepareChunks(storage, dimInfo, chunkSize, originalChunkCount, end); this._chunkCount = storage[dim].length; } var dataItem = new Array(dimLen); for (var idx = start; idx < end; idx++) { // NOTICE: Try not to write things into dataItem dataItem = rawData.getItem(idx, dataItem); // Each data item is value // [1, 2] // 2 // Bar chart, line chart which uses category axis // only gives the 'y' value. 'x' value is the indices of category // Use a tempValue to normalize the value to be a (x, y) value var chunkIndex = Math.floor(idx / chunkSize); var chunkOffset = idx % chunkSize; // Store the data by dimensions for (var k = 0; k < dimLen; k++) { var dim = dimensions[k]; var dimStorage = storage[dim][chunkIndex]; // PENDING NULL is empty or zero var val = this._dimValueGetter(dataItem, dim, idx, k); dimStorage[chunkOffset] = val; var dimRawExtent = rawExtent[dim]; val < dimRawExtent[0] && (dimRawExtent[0] = val); val > dimRawExtent[1] && (dimRawExtent[1] = val); } // ??? FIXME not check by pure but sourceFormat? // TODO refactor these logic. if (!rawData.pure) { var name = nameList[idx]; if (dataItem && name == null) { // If dataItem is {name: ...}, it has highest priority. // That is appropriate for many common cases. if (dataItem.name != null) { // There is no other place to persistent dataItem.name, // so save it to nameList. nameList[idx] = name = dataItem.name; } else if (nameDimIdx != null) { var nameDim = dimensions[nameDimIdx]; var nameDimChunk = storage[nameDim][chunkIndex]; if (nameDimChunk) { name = nameDimChunk[chunkOffset]; var ordinalMeta = dimensionInfoMap[nameDim].ordinalMeta; if (ordinalMeta && ordinalMeta.categories.length) { name = ordinalMeta.categories[name]; } } } } // Try using the id in option // id or name is used on dynamical data, mapping old and new items. var id = dataItem == null ? null : dataItem.id; if (id == null && name != null) { // Use name as id and add counter to avoid same name nameRepeatCount[name] = nameRepeatCount[name] || 0; id = name; if (nameRepeatCount[name] > 0) { id += '__ec__' + nameRepeatCount[name]; } nameRepeatCount[name]++; } id != null && (idList[idx] = id); } } if (!rawData.persistent && rawData.clean) { // Clean unused data if data source is typed array. rawData.clean(); } this._rawCount = this._count = end; // Reset data extent this._extent = {}; prepareInvertedIndex(this); }; function prepareChunks(storage, dimInfo, chunkSize, chunkCount, end) { var DataCtor = dataCtors[dimInfo.type]; var lastChunkIndex = chunkCount - 1; var dim = dimInfo.name; var resizeChunkArray = storage[dim][lastChunkIndex]; if (resizeChunkArray && resizeChunkArray.length < chunkSize) { var newStore = new DataCtor(Math.min(end - lastChunkIndex * chunkSize, chunkSize)); // The cost of the copy is probably inconsiderable // within the initial chunkSize. for (var j = 0; j < resizeChunkArray.length; j++) { newStore[j] = resizeChunkArray[j]; } storage[dim][lastChunkIndex] = newStore; } // Create new chunks. for (var k = chunkCount * chunkSize; k < end; k += chunkSize) { storage[dim].push(new DataCtor(Math.min(end - k, chunkSize))); } } function prepareInvertedIndex(list) { var invertedIndicesMap = list._invertedIndicesMap; each$1(invertedIndicesMap, function (invertedIndices, dim) { var dimInfo = list._dimensionInfos[dim]; // Currently, only dimensions that has ordinalMeta can create inverted indices. var ordinalMeta = dimInfo.ordinalMeta; if (ordinalMeta) { invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array( ordinalMeta.categories.length ); // The default value of TypedArray is 0. To avoid miss // mapping to 0, we should set it as INDEX_NOT_FOUND. for (var i = 0; i < invertedIndices.length; i++) { invertedIndices[i] = INDEX_NOT_FOUND; } for (var i = 0; i < list._count; i++) { // Only support the case that all values are distinct. invertedIndices[list.get(dim, i)] = i; } } }); } function getRawValueFromStore(list, dimIndex, rawIndex) { var val; if (dimIndex != null) { var chunkSize = list._chunkSize; var chunkIndex = Math.floor(rawIndex / chunkSize); var chunkOffset = rawIndex % chunkSize; var dim = list.dimensions[dimIndex]; var chunk = list._storage[dim][chunkIndex]; if (chunk) { val = chunk[chunkOffset]; var ordinalMeta = list._dimensionInfos[dim].ordinalMeta; if (ordinalMeta && ordinalMeta.categories.length) { val = ordinalMeta.categories[val]; } } } return val; } /** * @return {number} */ listProto.count = function () { return this._count; }; listProto.getIndices = function () { var newIndices; var indices = this._indices; if (indices) { var Ctor = indices.constructor; var thisCount = this._count; // `new Array(a, b, c)` is different from `new Uint32Array(a, b, c)`. if (Ctor === Array) { newIndices = new Ctor(thisCount); for (var i = 0; i < thisCount; i++) { newIndices[i] = indices[i]; } } else { newIndices = new Ctor(indices.buffer, 0, thisCount); } } else { var Ctor = getIndicesCtor(this); var newIndices = new Ctor(this.count()); for (var i = 0; i < newIndices.length; i++) { newIndices[i] = i; } } return newIndices; }; /** * Get value. Return NaN if idx is out of range. * @param {string} dim Dim must be concrete name. * @param {number} idx * @param {boolean} stack * @return {number} */ listProto.get = function (dim, idx /*, stack */) { if (!(idx >= 0 && idx < this._count)) { return NaN; } var storage = this._storage; if (!storage[dim]) { // TODO Warn ? return NaN; } idx = this.getRawIndex(idx); var chunkIndex = Math.floor(idx / this._chunkSize); var chunkOffset = idx % this._chunkSize; var chunkStore = storage[dim][chunkIndex]; var value = chunkStore[chunkOffset]; // FIXME ordinal data type is not stackable // if (stack) { // var dimensionInfo = this._dimensionInfos[dim]; // if (dimensionInfo && dimensionInfo.stackable) { // var stackedOn = this.stackedOn; // while (stackedOn) { // // Get no stacked data of stacked on // var stackedValue = stackedOn.get(dim, idx); // // Considering positive stack, negative stack and empty data // if ((value >= 0 && stackedValue > 0) // Positive stack // || (value <= 0 && stackedValue < 0) // Negative stack // ) { // value += stackedValue; // } // stackedOn = stackedOn.stackedOn; // } // } // } return value; }; /** * @param {string} dim concrete dim * @param {number} rawIndex * @return {number|string} */ listProto.getByRawIndex = function (dim, rawIdx) { if (!(rawIdx >= 0 && rawIdx < this._rawCount)) { return NaN; } var dimStore = this._storage[dim]; if (!dimStore) { // TODO Warn ? return NaN; } var chunkIndex = Math.floor(rawIdx / this._chunkSize); var chunkOffset = rawIdx % this._chunkSize; var chunkStore = dimStore[chunkIndex]; return chunkStore[chunkOffset]; }; /** * FIXME Use `get` on chrome maybe slow(in filterSelf and selectRange). * Hack a much simpler _getFast * @private */ listProto._getFast = function (dim, rawIdx) { var chunkIndex = Math.floor(rawIdx / this._chunkSize); var chunkOffset = rawIdx % this._chunkSize; var chunkStore = this._storage[dim][chunkIndex]; return chunkStore[chunkOffset]; }; /** * Get value for multi dimensions. * @param {Array.} [dimensions] If ignored, using all dimensions. * @param {number} idx * @return {number} */ listProto.getValues = function (dimensions, idx /*, stack */) { var values = []; if (!isArray(dimensions)) { // stack = idx; idx = dimensions; dimensions = this.dimensions; } for (var i = 0, len = dimensions.length; i < len; i++) { values.push(this.get(dimensions[i], idx /*, stack */)); } return values; }; /** * If value is NaN. Inlcuding '-' * Only check the coord dimensions. * @param {string} dim * @param {number} idx * @return {number} */ listProto.hasValue = function (idx) { var dataDimsOnCoord = this._dimensionsSummary.dataDimsOnCoord; for (var i = 0, len = dataDimsOnCoord.length; i < len; i++) { // Ordinal type originally can be string or number. // But when an ordinal type is used on coord, it can // not be string but only number. So we can also use isNaN. if (isNaN(this.get(dataDimsOnCoord[i], idx))) { return false; } } return true; }; /** * Get extent of data in one dimension * @param {string} dim * @param {boolean} stack */ listProto.getDataExtent = function (dim /*, stack */) { // Make sure use concrete dim as cache name. dim = this.getDimension(dim); var dimData = this._storage[dim]; var initialExtent = getInitialExtent(); // stack = !!((stack || false) && this.getCalculationInfo(dim)); if (!dimData) { return initialExtent; } // Make more strict checkings to ensure hitting cache. var currEnd = this.count(); // var cacheName = [dim, !!stack].join('_'); // var cacheName = dim; // Consider the most cases when using data zoom, `getDataExtent` // happened before filtering. We cache raw extent, which is not // necessary to be cleared and recalculated when restore data. var useRaw = !this._indices; // && !stack; var dimExtent; if (useRaw) { return this._rawExtent[dim].slice(); } dimExtent = this._extent[dim]; if (dimExtent) { return dimExtent.slice(); } dimExtent = initialExtent; var min = dimExtent[0]; var max = dimExtent[1]; for (var i = 0; i < currEnd; i++) { // var value = stack ? this.get(dim, i, true) : this._getFast(dim, this.getRawIndex(i)); var value = this._getFast(dim, this.getRawIndex(i)); value < min && (min = value); value > max && (max = value); } dimExtent = [min, max]; this._extent[dim] = dimExtent; return dimExtent; }; /** * Optimize for the scenario that data is filtered by a given extent. * Consider that if data amount is more than hundreds of thousand, * extent calculation will cost more than 10ms and the cache will * be erased because of the filtering. */ listProto.getApproximateExtent = function (dim /*, stack */) { dim = this.getDimension(dim); return this._approximateExtent[dim] || this.getDataExtent(dim /*, stack */); }; listProto.setApproximateExtent = function (extent, dim /*, stack */) { dim = this.getDimension(dim); this._approximateExtent[dim] = extent.slice(); }; /** * @param {string} key * @return {*} */ listProto.getCalculationInfo = function (key) { return this._calculationInfo[key]; }; /** * @param {string|Object} key or k-v object * @param {*} [value] */ listProto.setCalculationInfo = function (key, value) { isObject$4(key) ? extend(this._calculationInfo, key) : (this._calculationInfo[key] = value); }; /** * Get sum of data in one dimension * @param {string} dim */ listProto.getSum = function (dim /*, stack */) { var dimData = this._storage[dim]; var sum = 0; if (dimData) { for (var i = 0, len = this.count(); i < len; i++) { var value = this.get(dim, i /*, stack */); if (!isNaN(value)) { sum += value; } } } return sum; }; /** * Get median of data in one dimension * @param {string} dim */ listProto.getMedian = function (dim /*, stack */) { var dimDataArray = []; // map all data of one dimension this.each(dim, function (val, idx) { if (!isNaN(val)) { dimDataArray.push(val); } }); // TODO // Use quick select? // immutability & sort var sortedDimDataArray = [].concat(dimDataArray).sort(function (a, b) { return a - b; }); var len = this.count(); // calculate median return len === 0 ? 0 : len % 2 === 1 ? sortedDimDataArray[(len - 1) / 2] : (sortedDimDataArray[len / 2] + sortedDimDataArray[len / 2 - 1]) / 2; }; // /** // * Retreive the index with given value // * @param {string} dim Concrete dimension. // * @param {number} value // * @return {number} // */ // Currently incorrect: should return dataIndex but not rawIndex. // Do not fix it until this method is to be used somewhere. // FIXME Precision of float value // listProto.indexOf = function (dim, value) { // var storage = this._storage; // var dimData = storage[dim]; // var chunkSize = this._chunkSize; // if (dimData) { // for (var i = 0, len = this.count(); i < len; i++) { // var chunkIndex = Math.floor(i / chunkSize); // var chunkOffset = i % chunkSize; // if (dimData[chunkIndex][chunkOffset] === value) { // return i; // } // } // } // return -1; // }; /** * Only support the dimension which inverted index created. * Do not support other cases until required. * @param {string} concrete dim * @param {number|string} value * @return {number} rawIndex */ listProto.rawIndexOf = function (dim, value) { var invertedIndices = dim && this._invertedIndicesMap[dim]; if (__DEV__) { if (!invertedIndices) { throw new Error('Do not supported yet'); } } var rawIndex = invertedIndices[value]; if (rawIndex == null || isNaN(rawIndex)) { return INDEX_NOT_FOUND; } return rawIndex; }; /** * Retreive the index with given name * @param {number} idx * @param {number} name * @return {number} */ listProto.indexOfName = function (name) { for (var i = 0, len = this.count(); i < len; i++) { if (this.getName(i) === name) { return i; } } return -1; }; /** * Retreive the index with given raw data index * @param {number} idx * @param {number} name * @return {number} */ listProto.indexOfRawIndex = function (rawIndex) { if (rawIndex >= this._rawCount || rawIndex < 0) { return -1; } if (!this._indices) { return rawIndex; } // Indices are ascending var indices = this._indices; // If rawIndex === dataIndex var rawDataIndex = indices[rawIndex]; if (rawDataIndex != null && rawDataIndex < this._count && rawDataIndex === rawIndex) { return rawIndex; } var left = 0; var right = this._count - 1; while (left <= right) { var mid = (left + right) / 2 | 0; if (indices[mid] < rawIndex) { left = mid + 1; } else if (indices[mid] > rawIndex) { right = mid - 1; } else { return mid; } } return -1; }; /** * Retreive the index of nearest value * @param {string} dim * @param {number} value * @param {number} [maxDistance=Infinity] * @return {Array.} If and only if multiple indices has * the same value, they are put to the result. */ listProto.indicesOfNearest = function (dim, value, maxDistance) { var storage = this._storage; var dimData = storage[dim]; var nearestIndices = []; if (!dimData) { return nearestIndices; } if (maxDistance == null) { maxDistance = Infinity; } var minDist = Infinity; var minDiff = -1; var nearestIndicesLen = 0; // Check the test case of `test/ut/spec/data/List.js`. for (var i = 0, len = this.count(); i < len; i++) { var diff = value - this.get(dim, i); var dist = Math.abs(diff); if (dist <= maxDistance) { // When the `value` is at the middle of `this.get(dim, i)` and `this.get(dim, i+1)`, // we'd better not push both of them to `nearestIndices`, otherwise it is easy to // get more than one item in `nearestIndices` (more specifically, in `tooltip`). // So we chose the one that `diff >= 0` in this csae. // But if `this.get(dim, i)` and `this.get(dim, j)` get the same value, both of them // should be push to `nearestIndices`. if (dist < minDist || (dist === minDist && diff >= 0 && minDiff < 0) ) { minDist = dist; minDiff = diff; nearestIndicesLen = 0; } if (diff === minDiff) { nearestIndices[nearestIndicesLen++] = i; } } } nearestIndices.length = nearestIndicesLen; return nearestIndices; }; /** * Get raw data index * @param {number} idx * @return {number} */ listProto.getRawIndex = getRawIndexWithoutIndices; function getRawIndexWithoutIndices(idx) { return idx; } function getRawIndexWithIndices(idx) { if (idx < this._count && idx >= 0) { return this._indices[idx]; } return -1; } /** * Get raw data item * @param {number} idx * @return {number} */ listProto.getRawDataItem = function (idx) { if (!this._rawData.persistent) { var val = []; for (var i = 0; i < this.dimensions.length; i++) { var dim = this.dimensions[i]; val.push(this.get(dim, idx)); } return val; } else { return this._rawData.getItem(this.getRawIndex(idx)); } }; /** * @param {number} idx * @param {boolean} [notDefaultIdx=false] * @return {string} */ listProto.getName = function (idx) { var rawIndex = this.getRawIndex(idx); return this._nameList[rawIndex] || getRawValueFromStore(this, this._nameDimIdx, rawIndex) || ''; }; /** * @param {number} idx * @param {boolean} [notDefaultIdx=false] * @return {string} */ listProto.getId = function (idx) { return getId(this, this.getRawIndex(idx)); }; function getId(list, rawIndex) { var id = list._idList[rawIndex]; if (id == null) { id = getRawValueFromStore(list, list._idDimIdx, rawIndex); } if (id == null) { // FIXME Check the usage in graph, should not use prefix. id = ID_PREFIX + rawIndex; } return id; } function normalizeDimensions(dimensions) { if (!isArray(dimensions)) { dimensions = [dimensions]; } return dimensions; } function validateDimensions(list, dims) { for (var i = 0; i < dims.length; i++) { // stroage may be empty when no data, so use // dimensionInfos to check. if (!list._dimensionInfos[dims[i]]) { console.error('Unkown dimension ' + dims[i]); } } } /** * Data iteration * @param {string|Array.} * @param {Function} cb * @param {*} [context=this] * * @example * list.each('x', function (x, idx) {}); * list.each(['x', 'y'], function (x, y, idx) {}); * list.each(function (idx) {}) */ listProto.each = function (dims, cb, context, contextCompat) { 'use strict'; if (!this._count) { return; } if (typeof dims === 'function') { contextCompat = context; context = cb; cb = dims; dims = []; } // contextCompat just for compat echarts3 context = context || contextCompat || this; dims = map(normalizeDimensions(dims), this.getDimension, this); if (__DEV__) { validateDimensions(this, dims); } var dimSize = dims.length; for (var i = 0; i < this.count(); i++) { // Simple optimization switch (dimSize) { case 0: cb.call(context, i); break; case 1: cb.call(context, this.get(dims[0], i), i); break; case 2: cb.call(context, this.get(dims[0], i), this.get(dims[1], i), i); break; default: var k = 0; var value = []; for (; k < dimSize; k++) { value[k] = this.get(dims[k], i); } // Index value[k] = i; cb.apply(context, value); } } }; /** * Data filter * @param {string|Array.} * @param {Function} cb * @param {*} [context=this] */ listProto.filterSelf = function (dimensions, cb, context, contextCompat) { 'use strict'; if (!this._count) { return; } if (typeof dimensions === 'function') { contextCompat = context; context = cb; cb = dimensions; dimensions = []; } // contextCompat just for compat echarts3 context = context || contextCompat || this; dimensions = map( normalizeDimensions(dimensions), this.getDimension, this ); if (__DEV__) { validateDimensions(this, dimensions); } var count = this.count(); var Ctor = getIndicesCtor(this); var newIndices = new Ctor(count); var value = []; var dimSize = dimensions.length; var offset = 0; var dim0 = dimensions[0]; for (var i = 0; i < count; i++) { var keep; var rawIdx = this.getRawIndex(i); // Simple optimization if (dimSize === 0) { keep = cb.call(context, i); } else if (dimSize === 1) { var val = this._getFast(dim0, rawIdx); keep = cb.call(context, val, i); } else { for (var k = 0; k < dimSize; k++) { value[k] = this._getFast(dim0, rawIdx); } value[k] = i; keep = cb.apply(context, value); } if (keep) { newIndices[offset++] = rawIdx; } } // Set indices after filtered. if (offset < count) { this._indices = newIndices; } this._count = offset; // Reset data extent this._extent = {}; this.getRawIndex = this._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; return this; }; /** * Select data in range. (For optimization of filter) * (Manually inline code, support 5 million data filtering in data zoom.) */ listProto.selectRange = function (range) { 'use strict'; if (!this._count) { return; } var dimensions = []; for (var dim in range) { if (range.hasOwnProperty(dim)) { dimensions.push(dim); } } if (__DEV__) { validateDimensions(this, dimensions); } var dimSize = dimensions.length; if (!dimSize) { return; } var originalCount = this.count(); var Ctor = getIndicesCtor(this); var newIndices = new Ctor(originalCount); var offset = 0; var dim0 = dimensions[0]; var min = range[dim0][0]; var max = range[dim0][1]; var quickFinished = false; if (!this._indices) { // Extreme optimization for common case. About 2x faster in chrome. var idx = 0; if (dimSize === 1) { var dimStorage = this._storage[dimensions[0]]; for (var k = 0; k < this._chunkCount; k++) { var chunkStorage = dimStorage[k]; var len = Math.min(this._count - k * this._chunkSize, this._chunkSize); for (var i = 0; i < len; i++) { var val = chunkStorage[i]; // NaN will not be filtered. Consider the case, in line chart, empty // value indicates the line should be broken. But for the case like // scatter plot, a data item with empty value will not be rendered, // but the axis extent may be effected if some other dim of the data // item has value. Fortunately it is not a significant negative effect. if ( (val >= min && val <= max) || isNaN(val) ) { newIndices[offset++] = idx; } idx++; } } quickFinished = true; } else if (dimSize === 2) { var dimStorage = this._storage[dim0]; var dimStorage2 = this._storage[dimensions[1]]; var min2 = range[dimensions[1]][0]; var max2 = range[dimensions[1]][1]; for (var k = 0; k < this._chunkCount; k++) { var chunkStorage = dimStorage[k]; var chunkStorage2 = dimStorage2[k]; var len = Math.min(this._count - k * this._chunkSize, this._chunkSize); for (var i = 0; i < len; i++) { var val = chunkStorage[i]; var val2 = chunkStorage2[i]; // Do not filter NaN, see comment above. if (( (val >= min && val <= max) || isNaN(val) ) && ( (val2 >= min2 && val2 <= max2) || isNaN(val2) ) ) { newIndices[offset++] = idx; } idx++; } } quickFinished = true; } } if (!quickFinished) { if (dimSize === 1) { for (var i = 0; i < originalCount; i++) { var rawIndex = this.getRawIndex(i); var val = this._getFast(dim0, rawIndex); // Do not filter NaN, see comment above. if ( (val >= min && val <= max) || isNaN(val) ) { newIndices[offset++] = rawIndex; } } } else { for (var i = 0; i < originalCount; i++) { var keep = true; var rawIndex = this.getRawIndex(i); for (var k = 0; k < dimSize; k++) { var dimk = dimensions[k]; var val = this._getFast(dim, rawIndex); // Do not filter NaN, see comment above. if (val < range[dimk][0] || val > range[dimk][1]) { keep = false; } } if (keep) { newIndices[offset++] = this.getRawIndex(i); } } } } // Set indices after filtered. if (offset < originalCount) { this._indices = newIndices; } this._count = offset; // Reset data extent this._extent = {}; this.getRawIndex = this._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; return this; }; /** * Data mapping to a plain array * @param {string|Array.} [dimensions] * @param {Function} cb * @param {*} [context=this] * @return {Array} */ listProto.mapArray = function (dimensions, cb, context, contextCompat) { 'use strict'; if (typeof dimensions === 'function') { contextCompat = context; context = cb; cb = dimensions; dimensions = []; } // contextCompat just for compat echarts3 context = context || contextCompat || this; var result = []; this.each(dimensions, function () { result.push(cb && cb.apply(this, arguments)); }, context); return result; }; // Data in excludeDimensions is copied, otherwise transfered. function cloneListForMapAndSample(original, excludeDimensions) { var allDimensions = original.dimensions; var list = new List( map(allDimensions, original.getDimensionInfo, original), original.hostModel ); // FIXME If needs stackedOn, value may already been stacked transferProperties(list, original); var storage = list._storage = {}; var originalStorage = original._storage; // Init storage for (var i = 0; i < allDimensions.length; i++) { var dim = allDimensions[i]; if (originalStorage[dim]) { // Notice that we do not reset invertedIndicesMap here, becuase // there is no scenario of mapping or sampling ordinal dimension. if (indexOf(excludeDimensions, dim) >= 0) { storage[dim] = cloneDimStore(originalStorage[dim]); list._rawExtent[dim] = getInitialExtent(); list._extent[dim] = null; } else { // Direct reference for other dimensions storage[dim] = originalStorage[dim]; } } } return list; } function cloneDimStore(originalDimStore) { var newDimStore = new Array(originalDimStore.length); for (var j = 0; j < originalDimStore.length; j++) { newDimStore[j] = cloneChunk(originalDimStore[j]); } return newDimStore; } function getInitialExtent() { return [Infinity, -Infinity]; } /** * Data mapping to a new List with given dimensions * @param {string|Array.} dimensions * @param {Function} cb * @param {*} [context=this] * @return {Array} */ listProto.map = function (dimensions, cb, context, contextCompat) { 'use strict'; // contextCompat just for compat echarts3 context = context || contextCompat || this; dimensions = map( normalizeDimensions(dimensions), this.getDimension, this ); if (__DEV__) { validateDimensions(this, dimensions); } var list = cloneListForMapAndSample(this, dimensions); // Following properties are all immutable. // So we can reference to the same value list._indices = this._indices; list.getRawIndex = list._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; var storage = list._storage; var tmpRetValue = []; var chunkSize = this._chunkSize; var dimSize = dimensions.length; var dataCount = this.count(); var values = []; var rawExtent = list._rawExtent; for (var dataIndex = 0; dataIndex < dataCount; dataIndex++) { for (var dimIndex = 0; dimIndex < dimSize; dimIndex++) { values[dimIndex] = this.get(dimensions[dimIndex], dataIndex /*, stack */); } values[dimSize] = dataIndex; var retValue = cb && cb.apply(context, values); if (retValue != null) { // a number or string (in oridinal dimension)? if (typeof retValue !== 'object') { tmpRetValue[0] = retValue; retValue = tmpRetValue; } var rawIndex = this.getRawIndex(dataIndex); var chunkIndex = Math.floor(rawIndex / chunkSize); var chunkOffset = rawIndex % chunkSize; for (var i = 0; i < retValue.length; i++) { var dim = dimensions[i]; var val = retValue[i]; var rawExtentOnDim = rawExtent[dim]; var dimStore = storage[dim]; if (dimStore) { dimStore[chunkIndex][chunkOffset] = val; } if (val < rawExtentOnDim[0]) { rawExtentOnDim[0] = val; } if (val > rawExtentOnDim[1]) { rawExtentOnDim[1] = val; } } } } return list; }; /** * Large data down sampling on given dimension * @param {string} dimension * @param {number} rate * @param {Function} sampleValue * @param {Function} sampleIndex Sample index for name and id */ listProto.downSample = function (dimension, rate, sampleValue, sampleIndex) { var list = cloneListForMapAndSample(this, [dimension]); var targetStorage = list._storage; var frameValues = []; var frameSize = Math.floor(1 / rate); var dimStore = targetStorage[dimension]; var len = this.count(); var chunkSize = this._chunkSize; var rawExtentOnDim = list._rawExtent[dimension]; var newIndices = new (getIndicesCtor(this))(len); var offset = 0; for (var i = 0; i < len; i += frameSize) { // Last frame if (frameSize > len - i) { frameSize = len - i; frameValues.length = frameSize; } for (var k = 0; k < frameSize; k++) { var dataIdx = this.getRawIndex(i + k); var originalChunkIndex = Math.floor(dataIdx / chunkSize); var originalChunkOffset = dataIdx % chunkSize; frameValues[k] = dimStore[originalChunkIndex][originalChunkOffset]; } var value = sampleValue(frameValues); var sampleFrameIdx = this.getRawIndex( Math.min(i + sampleIndex(frameValues, value) || 0, len - 1) ); var sampleChunkIndex = Math.floor(sampleFrameIdx / chunkSize); var sampleChunkOffset = sampleFrameIdx % chunkSize; // Only write value on the filtered data dimStore[sampleChunkIndex][sampleChunkOffset] = value; if (value < rawExtentOnDim[0]) { rawExtentOnDim[0] = value; } if (value > rawExtentOnDim[1]) { rawExtentOnDim[1] = value; } newIndices[offset++] = sampleFrameIdx; } list._count = offset; list._indices = newIndices; list.getRawIndex = getRawIndexWithIndices; return list; }; /** * Get model of one data item. * * @param {number} idx */ // FIXME Model proxy ? listProto.getItemModel = function (idx) { var hostModel = this.hostModel; return new Model(this.getRawDataItem(idx), hostModel, hostModel && hostModel.ecModel); }; /** * Create a data differ * @param {module:echarts/data/List} otherList * @return {module:echarts/data/DataDiffer} */ listProto.diff = function (otherList) { var thisList = this; return new DataDiffer( otherList ? otherList.getIndices() : [], this.getIndices(), function (idx) { return getId(otherList, idx); }, function (idx) { return getId(thisList, idx); } ); }; /** * Get visual property. * @param {string} key */ listProto.getVisual = function (key) { var visual = this._visual; return visual && visual[key]; }; /** * Set visual property * @param {string|Object} key * @param {*} [value] * * @example * setVisual('color', color); * setVisual({ * 'color': color * }); */ listProto.setVisual = function (key, val) { if (isObject$4(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { this.setVisual(name, key[name]); } } return; } this._visual = this._visual || {}; this._visual[key] = val; }; /** * Set layout property. * @param {string|Object} key * @param {*} [val] */ listProto.setLayout = function (key, val) { if (isObject$4(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { this.setLayout(name, key[name]); } } return; } this._layout[key] = val; }; /** * Get layout property. * @param {string} key. * @return {*} */ listProto.getLayout = function (key) { return this._layout[key]; }; /** * Get layout of single data item * @param {number} idx */ listProto.getItemLayout = function (idx) { return this._itemLayouts[idx]; }; /** * Set layout of single data item * @param {number} idx * @param {Object} layout * @param {boolean=} [merge=false] */ listProto.setItemLayout = function (idx, layout, merge$$1) { this._itemLayouts[idx] = merge$$1 ? extend(this._itemLayouts[idx] || {}, layout) : layout; }; /** * Clear all layout of single data item */ listProto.clearItemLayouts = function () { this._itemLayouts.length = 0; }; /** * Get visual property of single data item * @param {number} idx * @param {string} key * @param {boolean} [ignoreParent=false] */ listProto.getItemVisual = function (idx, key, ignoreParent) { var itemVisual = this._itemVisuals[idx]; var val = itemVisual && itemVisual[key]; if (val == null && !ignoreParent) { // Use global visual property return this.getVisual(key); } return val; }; /** * Set visual property of single data item * * @param {number} idx * @param {string|Object} key * @param {*} [value] * * @example * setItemVisual(0, 'color', color); * setItemVisual(0, { * 'color': color * }); */ listProto.setItemVisual = function (idx, key, value) { var itemVisual = this._itemVisuals[idx] || {}; var hasItemVisual = this.hasItemVisual; this._itemVisuals[idx] = itemVisual; if (isObject$4(key)) { for (var name in key) { if (key.hasOwnProperty(name)) { itemVisual[name] = key[name]; hasItemVisual[name] = true; } } return; } itemVisual[key] = value; hasItemVisual[key] = true; }; /** * Clear itemVisuals and list visual. */ listProto.clearAllVisual = function () { this._visual = {}; this._itemVisuals = []; this.hasItemVisual = {}; }; var setItemDataAndSeriesIndex = function (child) { child.seriesIndex = this.seriesIndex; child.dataIndex = this.dataIndex; child.dataType = this.dataType; }; /** * Set graphic element relative to data. It can be set as null * @param {number} idx * @param {module:zrender/Element} [el] */ listProto.setItemGraphicEl = function (idx, el) { var hostModel = this.hostModel; if (el) { // Add data index and series index for indexing the data by element // Useful in tooltip el.dataIndex = idx; el.dataType = this.dataType; el.seriesIndex = hostModel && hostModel.seriesIndex; if (el.type === 'group') { el.traverse(setItemDataAndSeriesIndex, el); } } this._graphicEls[idx] = el; }; /** * @param {number} idx * @return {module:zrender/Element} */ listProto.getItemGraphicEl = function (idx) { return this._graphicEls[idx]; }; /** * @param {Function} cb * @param {*} context */ listProto.eachItemGraphicEl = function (cb, context) { each$1(this._graphicEls, function (el, idx) { if (el) { cb && cb.call(context, el, idx); } }); }; /** * Shallow clone a new list except visual and layout properties, and graph elements. * New list only change the indices. */ listProto.cloneShallow = function (list) { if (!list) { var dimensionInfoList = map(this.dimensions, this.getDimensionInfo, this); list = new List(dimensionInfoList, this.hostModel); } // FIXME list._storage = this._storage; transferProperties(list, this); // Clone will not change the data extent and indices if (this._indices) { var Ctor = this._indices.constructor; list._indices = new Ctor(this._indices); } else { list._indices = null; } list.getRawIndex = list._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; return list; }; /** * Wrap some method to add more feature * @param {string} methodName * @param {Function} injectFunction */ listProto.wrapMethod = function (methodName, injectFunction) { var originalMethod = this[methodName]; if (typeof originalMethod !== 'function') { return; } this.__wrappedMethods = this.__wrappedMethods || []; this.__wrappedMethods.push(methodName); this[methodName] = function () { var res = originalMethod.apply(this, arguments); return injectFunction.apply(this, [res].concat(slice(arguments))); }; }; // Methods that create a new list based on this list should be listed here. // Notice that those method should `RETURN` the new list. listProto.TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'map']; // Methods that change indices of this list should be listed here. listProto.CHANGABLE_METHODS = ['filterSelf', 'selectRange']; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @deprecated * Use `echarts/data/helper/createDimensions` instead. */ /** * @see {module:echarts/test/ut/spec/data/completeDimensions} * * This method builds the relationship between: * + "what the coord sys or series requires (see `sysDims`)", * + "what the user defines (in `encode` and `dimensions`, see `opt.dimsDef` and `opt.encodeDef`)" * + "what the data source provids (see `source`)". * * Some guess strategy will be adapted if user does not define something. * If no 'value' dimension specified, the first no-named dimension will be * named as 'value'. * * @param {Array.} sysDims Necessary dimensions, like ['x', 'y'], which * provides not only dim template, but also default order. * properties: 'name', 'type', 'displayName'. * `name` of each item provides default coord name. * [{dimsDef: [string|Object, ...]}, ...] dimsDef of sysDim item provides default dim name, and * provide dims count that the sysDim required. * [{ordinalMeta}] can be specified. * @param {module:echarts/data/Source|Array|Object} source or data (for compatibal with pervious) * @param {Object} [opt] * @param {Array.} [opt.dimsDef] option.series.dimensions User defined dimensions * For example: ['asdf', {name, type}, ...]. * @param {Object|HashMap} [opt.encodeDef] option.series.encode {x: 2, y: [3, 1], tooltip: [1, 2], label: 3} * @param {Function} [opt.encodeDefaulter] Called if no `opt.encodeDef` exists. * If not specified, auto find the next available data dim. * param source {module:data/Source} * param dimCount {number} * return {Object} encode Never be `null/undefined`. * @param {string} [opt.generateCoord] Generate coord dim with the given name. * If not specified, extra dim names will be: * 'value', 'value0', 'value1', ... * @param {number} [opt.generateCoordCount] By default, the generated dim name is `generateCoord`. * If `generateCoordCount` specified, the generated dim names will be: * `generateCoord` + 0, `generateCoord` + 1, ... * can be Infinity, indicate that use all of the remain columns. * @param {number} [opt.dimCount] If not specified, guess by the first data item. * @return {Array.} */ function completeDimensions(sysDims, source, opt) { if (!Source.isInstance(source)) { source = Source.seriesDataToSource(source); } opt = opt || {}; sysDims = (sysDims || []).slice(); var dimsDef = (opt.dimsDef || []).slice(); var dataDimNameMap = createHashMap(); var coordDimNameMap = createHashMap(); // var valueCandidate; var result = []; var dimCount = getDimCount(source, sysDims, dimsDef, opt.dimCount); // Apply user defined dims (`name` and `type`) and init result. for (var i = 0; i < dimCount; i++) { var dimDefItem = dimsDef[i] = extend( {}, isObject$1(dimsDef[i]) ? dimsDef[i] : {name: dimsDef[i]} ); var userDimName = dimDefItem.name; var resultItem = result[i] = new DataDimensionInfo(); // Name will be applied later for avoiding duplication. if (userDimName != null && dataDimNameMap.get(userDimName) == null) { // Only if `series.dimensions` is defined in option // displayName, will be set, and dimension will be diplayed vertically in // tooltip by default. resultItem.name = resultItem.displayName = userDimName; dataDimNameMap.set(userDimName, i); } dimDefItem.type != null && (resultItem.type = dimDefItem.type); dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName); } var encodeDef = opt.encodeDef; if (!encodeDef && opt.encodeDefaulter) { encodeDef = opt.encodeDefaulter(source, dimCount); } encodeDef = createHashMap(encodeDef); // Set `coordDim` and `coordDimIndex` by `encodeDef` and normalize `encodeDef`. encodeDef.each(function (dataDims, coordDim) { dataDims = normalizeToArray(dataDims).slice(); // Note: It is allowed that `dataDims.length` is `0`, e.g., options is // `{encode: {x: -1, y: 1}}`. Should not filter anything in // this case. if (dataDims.length === 1 && !isString(dataDims[0]) && dataDims[0] < 0) { encodeDef.set(coordDim, false); return; } var validDataDims = encodeDef.set(coordDim, []); each$1(dataDims, function (resultDimIdx, idx) { // The input resultDimIdx can be dim name or index. isString(resultDimIdx) && (resultDimIdx = dataDimNameMap.get(resultDimIdx)); if (resultDimIdx != null && resultDimIdx < dimCount) { validDataDims[idx] = resultDimIdx; applyDim(result[resultDimIdx], coordDim, idx); } }); }); // Apply templetes and default order from `sysDims`. var availDimIdx = 0; each$1(sysDims, function (sysDimItem, sysDimIndex) { var coordDim; var sysDimItem; var sysDimItemDimsDef; var sysDimItemOtherDims; if (isString(sysDimItem)) { coordDim = sysDimItem; sysDimItem = {}; } else { coordDim = sysDimItem.name; var ordinalMeta = sysDimItem.ordinalMeta; sysDimItem.ordinalMeta = null; sysDimItem = clone(sysDimItem); sysDimItem.ordinalMeta = ordinalMeta; // `coordDimIndex` should not be set directly. sysDimItemDimsDef = sysDimItem.dimsDef; sysDimItemOtherDims = sysDimItem.otherDims; sysDimItem.name = sysDimItem.coordDim = sysDimItem.coordDimIndex = sysDimItem.dimsDef = sysDimItem.otherDims = null; } var dataDims = encodeDef.get(coordDim); // negative resultDimIdx means no need to mapping. if (dataDims === false) { return; } var dataDims = normalizeToArray(dataDims); // dimensions provides default dim sequences. if (!dataDims.length) { for (var i = 0; i < (sysDimItemDimsDef && sysDimItemDimsDef.length || 1); i++) { while (availDimIdx < result.length && result[availDimIdx].coordDim != null) { availDimIdx++; } availDimIdx < result.length && dataDims.push(availDimIdx++); } } // Apply templates. each$1(dataDims, function (resultDimIdx, coordDimIndex) { var resultItem = result[resultDimIdx]; applyDim(defaults(resultItem, sysDimItem), coordDim, coordDimIndex); if (resultItem.name == null && sysDimItemDimsDef) { var sysDimItemDimsDefItem = sysDimItemDimsDef[coordDimIndex]; !isObject$1(sysDimItemDimsDefItem) && (sysDimItemDimsDefItem = {name: sysDimItemDimsDefItem}); resultItem.name = resultItem.displayName = sysDimItemDimsDefItem.name; resultItem.defaultTooltip = sysDimItemDimsDefItem.defaultTooltip; } // FIXME refactor, currently only used in case: {otherDims: {tooltip: false}} sysDimItemOtherDims && defaults(resultItem.otherDims, sysDimItemOtherDims); }); }); function applyDim(resultItem, coordDim, coordDimIndex) { if (OTHER_DIMENSIONS.get(coordDim) != null) { resultItem.otherDims[coordDim] = coordDimIndex; } else { resultItem.coordDim = coordDim; resultItem.coordDimIndex = coordDimIndex; coordDimNameMap.set(coordDim, true); } } // Make sure the first extra dim is 'value'. var generateCoord = opt.generateCoord; var generateCoordCount = opt.generateCoordCount; var fromZero = generateCoordCount != null; generateCoordCount = generateCoord ? (generateCoordCount || 1) : 0; var extra = generateCoord || 'value'; // Set dim `name` and other `coordDim` and other props. for (var resultDimIdx = 0; resultDimIdx < dimCount; resultDimIdx++) { var resultItem = result[resultDimIdx] = result[resultDimIdx] || new DataDimensionInfo(); var coordDim = resultItem.coordDim; if (coordDim == null) { resultItem.coordDim = genName( extra, coordDimNameMap, fromZero ); resultItem.coordDimIndex = 0; if (!generateCoord || generateCoordCount <= 0) { resultItem.isExtraCoord = true; } generateCoordCount--; } resultItem.name == null && (resultItem.name = genName( resultItem.coordDim, dataDimNameMap )); if (resultItem.type == null && ( guessOrdinal(source, resultDimIdx, resultItem.name) === BE_ORDINAL.Must // Consider the case: // { // dataset: {source: [ // ['2001', 123], // ['2002', 456], // ... // ['The others', 987], // ]}, // series: {type: 'pie'} // } // The first colum should better be treated as a "ordinal" although it // might not able to be detected as an "ordinal" by `guessOrdinal`. || (resultItem.isExtraCoord && (resultItem.otherDims.itemName != null || resultItem.otherDims.seriesName != null ) ) ) ) { resultItem.type = 'ordinal'; } } return result; } // ??? TODO // Originally detect dimCount by data[0]. Should we // optimize it to only by sysDims and dimensions and encode. // So only necessary dims will be initialized. // But // (1) custom series should be considered. where other dims // may be visited. // (2) sometimes user need to calcualte bubble size or use visualMap // on other dimensions besides coordSys needed. // So, dims that is not used by system, should be shared in storage? function getDimCount(source, sysDims, dimsDef, optDimCount) { // Note that the result dimCount should not small than columns count // of data, otherwise `dataDimNameMap` checking will be incorrect. var dimCount = Math.max( source.dimensionsDetectCount || 1, sysDims.length, dimsDef.length, optDimCount || 0 ); each$1(sysDims, function (sysDimItem) { var sysDimItemDimsDef = sysDimItem.dimsDef; sysDimItemDimsDef && (dimCount = Math.max(dimCount, sysDimItemDimsDef.length)); }); return dimCount; } function genName(name, map$$1, fromZero) { if (fromZero || map$$1.get(name) != null) { var i = 0; while (map$$1.get(name + i) != null) { i++; } name += i; } map$$1.set(name, true); return name; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Substitute `completeDimensions`. * `completeDimensions` is to be deprecated. */ /** * @param {module:echarts/data/Source|module:echarts/data/List} source or data. * @param {Object|Array} [opt] * @param {Array.} [opt.coordDimensions=[]] * @param {number} [opt.dimensionsCount] * @param {string} [opt.generateCoord] * @param {string} [opt.generateCoordCount] * @param {Array.} [opt.dimensionsDefine=source.dimensionsDefine] Overwrite source define. * @param {Object|HashMap} [opt.encodeDefine=source.encodeDefine] Overwrite source define. * @param {Function} [opt.encodeDefaulter] Make default encode if user not specified. * @return {Array.} dimensionsInfo */ var createDimensions = function (source, opt) { opt = opt || {}; return completeDimensions(opt.coordDimensions || [], source, { dimsDef: opt.dimensionsDefine || source.dimensionsDefine, encodeDef: opt.encodeDefine || source.encodeDefine, dimCount: opt.dimensionsCount, encodeDefaulter: opt.encodeDefaulter, generateCoord: opt.generateCoord, generateCoordCount: opt.generateCoordCount }); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Helper for model references. * There are many manners to refer axis/coordSys. */ // TODO // merge relevant logic to this file? // check: "modelHelper" of tooltip and "BrushTargetManager". /** * @class * For example: * { * coordSysName: 'cartesian2d', * coordSysDims: ['x', 'y', ...], * axisMap: HashMap({ * x: xAxisModel, * y: yAxisModel * }), * categoryAxisMap: HashMap({ * x: xAxisModel, * y: undefined * }), * // The index of the first category axis in `coordSysDims`. * // `null/undefined` means no category axis exists. * firstCategoryDimIndex: 1, * // To replace user specified encode. * } */ function CoordSysInfo(coordSysName) { /** * @type {string} */ this.coordSysName = coordSysName; /** * @type {Array.} */ this.coordSysDims = []; /** * @type {module:zrender/core/util#HashMap} */ this.axisMap = createHashMap(); /** * @type {module:zrender/core/util#HashMap} */ this.categoryAxisMap = createHashMap(); /** * @type {number} */ this.firstCategoryDimIndex = null; } /** * @return {module:model/referHelper#CoordSysInfo} */ function getCoordSysInfoBySeries(seriesModel) { var coordSysName = seriesModel.get('coordinateSystem'); var result = new CoordSysInfo(coordSysName); var fetch = fetchers[coordSysName]; if (fetch) { fetch(seriesModel, result, result.axisMap, result.categoryAxisMap); return result; } } var fetchers = { cartesian2d: function (seriesModel, result, axisMap, categoryAxisMap) { var xAxisModel = seriesModel.getReferringComponents('xAxis')[0]; var yAxisModel = seriesModel.getReferringComponents('yAxis')[0]; if (__DEV__) { if (!xAxisModel) { throw new Error('xAxis "' + retrieve( seriesModel.get('xAxisIndex'), seriesModel.get('xAxisId'), 0 ) + '" not found'); } if (!yAxisModel) { throw new Error('yAxis "' + retrieve( seriesModel.get('xAxisIndex'), seriesModel.get('yAxisId'), 0 ) + '" not found'); } } result.coordSysDims = ['x', 'y']; axisMap.set('x', xAxisModel); axisMap.set('y', yAxisModel); if (isCategory(xAxisModel)) { categoryAxisMap.set('x', xAxisModel); result.firstCategoryDimIndex = 0; } if (isCategory(yAxisModel)) { categoryAxisMap.set('y', yAxisModel); result.firstCategoryDimIndex == null & (result.firstCategoryDimIndex = 1); } }, singleAxis: function (seriesModel, result, axisMap, categoryAxisMap) { var singleAxisModel = seriesModel.getReferringComponents('singleAxis')[0]; if (__DEV__) { if (!singleAxisModel) { throw new Error('singleAxis should be specified.'); } } result.coordSysDims = ['single']; axisMap.set('single', singleAxisModel); if (isCategory(singleAxisModel)) { categoryAxisMap.set('single', singleAxisModel); result.firstCategoryDimIndex = 0; } }, polar: function (seriesModel, result, axisMap, categoryAxisMap) { var polarModel = seriesModel.getReferringComponents('polar')[0]; var radiusAxisModel = polarModel.findAxisModel('radiusAxis'); var angleAxisModel = polarModel.findAxisModel('angleAxis'); if (__DEV__) { if (!angleAxisModel) { throw new Error('angleAxis option not found'); } if (!radiusAxisModel) { throw new Error('radiusAxis option not found'); } } result.coordSysDims = ['radius', 'angle']; axisMap.set('radius', radiusAxisModel); axisMap.set('angle', angleAxisModel); if (isCategory(radiusAxisModel)) { categoryAxisMap.set('radius', radiusAxisModel); result.firstCategoryDimIndex = 0; } if (isCategory(angleAxisModel)) { categoryAxisMap.set('angle', angleAxisModel); result.firstCategoryDimIndex == null && (result.firstCategoryDimIndex = 1); } }, geo: function (seriesModel, result, axisMap, categoryAxisMap) { result.coordSysDims = ['lng', 'lat']; }, parallel: function (seriesModel, result, axisMap, categoryAxisMap) { var ecModel = seriesModel.ecModel; var parallelModel = ecModel.getComponent( 'parallel', seriesModel.get('parallelIndex') ); var coordSysDims = result.coordSysDims = parallelModel.dimensions.slice(); each$1(parallelModel.parallelAxisIndex, function (axisIndex, index) { var axisModel = ecModel.getComponent('parallelAxis', axisIndex); var axisDim = coordSysDims[index]; axisMap.set(axisDim, axisModel); if (isCategory(axisModel) && result.firstCategoryDimIndex == null) { categoryAxisMap.set(axisDim, axisModel); result.firstCategoryDimIndex = index; } }); } }; function isCategory(axisModel) { return axisModel.get('type') === 'category'; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Note that it is too complicated to support 3d stack by value * (have to create two-dimension inverted index), so in 3d case * we just support that stacked by index. * * @param {module:echarts/model/Series} seriesModel * @param {Array.} dimensionInfoList The same as the input of . * The input dimensionInfoList will be modified. * @param {Object} [opt] * @param {boolean} [opt.stackedCoordDimension=''] Specify a coord dimension if needed. * @param {boolean} [opt.byIndex=false] * @return {Object} calculationInfo * { * stackedDimension: string * stackedByDimension: string * isStackedByIndex: boolean * stackedOverDimension: string * stackResultDimension: string * } */ function enableDataStack(seriesModel, dimensionInfoList, opt) { opt = opt || {}; var byIndex = opt.byIndex; var stackedCoordDimension = opt.stackedCoordDimension; // Compatibal: when `stack` is set as '', do not stack. var mayStack = !!(seriesModel && seriesModel.get('stack')); var stackedByDimInfo; var stackedDimInfo; var stackResultDimension; var stackedOverDimension; each$1(dimensionInfoList, function (dimensionInfo, index) { if (isString(dimensionInfo)) { dimensionInfoList[index] = dimensionInfo = {name: dimensionInfo}; } if (mayStack && !dimensionInfo.isExtraCoord) { // Find the first ordinal dimension as the stackedByDimInfo. if (!byIndex && !stackedByDimInfo && dimensionInfo.ordinalMeta) { stackedByDimInfo = dimensionInfo; } // Find the first stackable dimension as the stackedDimInfo. if (!stackedDimInfo && dimensionInfo.type !== 'ordinal' && dimensionInfo.type !== 'time' && (!stackedCoordDimension || stackedCoordDimension === dimensionInfo.coordDim) ) { stackedDimInfo = dimensionInfo; } } }); if (stackedDimInfo && !byIndex && !stackedByDimInfo) { // Compatible with previous design, value axis (time axis) only stack by index. // It may make sense if the user provides elaborately constructed data. byIndex = true; } // Add stack dimension, they can be both calculated by coordinate system in `unionExtent`. // That put stack logic in List is for using conveniently in echarts extensions, but it // might not be a good way. if (stackedDimInfo) { // Use a weird name that not duplicated with other names. stackResultDimension = '__\0ecstackresult'; stackedOverDimension = '__\0ecstackedover'; // Create inverted index to fast query index by value. if (stackedByDimInfo) { stackedByDimInfo.createInvertedIndices = true; } var stackedDimCoordDim = stackedDimInfo.coordDim; var stackedDimType = stackedDimInfo.type; var stackedDimCoordIndex = 0; each$1(dimensionInfoList, function (dimensionInfo) { if (dimensionInfo.coordDim === stackedDimCoordDim) { stackedDimCoordIndex++; } }); dimensionInfoList.push({ name: stackResultDimension, coordDim: stackedDimCoordDim, coordDimIndex: stackedDimCoordIndex, type: stackedDimType, isExtraCoord: true, isCalculationCoord: true }); stackedDimCoordIndex++; dimensionInfoList.push({ name: stackedOverDimension, // This dimension contains stack base (generally, 0), so do not set it as // `stackedDimCoordDim` to avoid extent calculation, consider log scale. coordDim: stackedOverDimension, coordDimIndex: stackedDimCoordIndex, type: stackedDimType, isExtraCoord: true, isCalculationCoord: true }); } return { stackedDimension: stackedDimInfo && stackedDimInfo.name, stackedByDimension: stackedByDimInfo && stackedByDimInfo.name, isStackedByIndex: byIndex, stackedOverDimension: stackedOverDimension, stackResultDimension: stackResultDimension }; } /** * @param {module:echarts/data/List} data * @param {string} stackedDim */ function isDimensionStacked(data, stackedDim /*, stackedByDim*/) { // Each single series only maps to one pair of axis. So we do not need to // check stackByDim, whatever stacked by a dimension or stacked by index. return !!stackedDim && stackedDim === data.getCalculationInfo('stackedDimension'); // && ( // stackedByDim != null // ? stackedByDim === data.getCalculationInfo('stackedByDimension') // : data.getCalculationInfo('isStackedByIndex') // ); } /** * @param {module:echarts/data/List} data * @param {string} targetDim * @param {string} [stackedByDim] If not input this parameter, check whether * stacked by index. * @return {string} dimension */ function getStackedDimension(data, targetDim) { return isDimensionStacked(data, targetDim) ? data.getCalculationInfo('stackResultDimension') : targetDim; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {module:echarts/data/Source|Array} source Or raw data. * @param {module:echarts/model/Series} seriesModel * @param {Object} [opt] * @param {string} [opt.generateCoord] * @param {boolean} [opt.useEncodeDefaulter] */ function createListFromArray(source, seriesModel, opt) { opt = opt || {}; if (!Source.isInstance(source)) { source = Source.seriesDataToSource(source); } var coordSysName = seriesModel.get('coordinateSystem'); var registeredCoordSys = CoordinateSystemManager.get(coordSysName); var coordSysInfo = getCoordSysInfoBySeries(seriesModel); var coordSysDimDefs; if (coordSysInfo) { coordSysDimDefs = map(coordSysInfo.coordSysDims, function (dim) { var dimInfo = {name: dim}; var axisModel = coordSysInfo.axisMap.get(dim); if (axisModel) { var axisType = axisModel.get('type'); dimInfo.type = getDimensionTypeByAxis(axisType); // dimInfo.stackable = isStackable(axisType); } return dimInfo; }); } if (!coordSysDimDefs) { // Get dimensions from registered coordinate system coordSysDimDefs = (registeredCoordSys && ( registeredCoordSys.getDimensionsInfo ? registeredCoordSys.getDimensionsInfo() : registeredCoordSys.dimensions.slice() )) || ['x', 'y']; } var dimInfoList = createDimensions(source, { coordDimensions: coordSysDimDefs, generateCoord: opt.generateCoord, encodeDefaulter: opt.useEncodeDefaulter ? curry(makeSeriesEncodeForAxisCoordSys, coordSysDimDefs, seriesModel) : null }); var firstCategoryDimIndex; var hasNameEncode; coordSysInfo && each$1(dimInfoList, function (dimInfo, dimIndex) { var coordDim = dimInfo.coordDim; var categoryAxisModel = coordSysInfo.categoryAxisMap.get(coordDim); if (categoryAxisModel) { if (firstCategoryDimIndex == null) { firstCategoryDimIndex = dimIndex; } dimInfo.ordinalMeta = categoryAxisModel.getOrdinalMeta(); } if (dimInfo.otherDims.itemName != null) { hasNameEncode = true; } }); if (!hasNameEncode && firstCategoryDimIndex != null) { dimInfoList[firstCategoryDimIndex].otherDims.itemName = 0; } var stackCalculationInfo = enableDataStack(seriesModel, dimInfoList); var list = new List(dimInfoList, seriesModel); list.setCalculationInfo(stackCalculationInfo); var dimValueGetter = (firstCategoryDimIndex != null && isNeedCompleteOrdinalData(source)) ? function (itemOpt, dimName, dataIndex, dimIndex) { // Use dataIndex as ordinal value in categoryAxis return dimIndex === firstCategoryDimIndex ? dataIndex : this.defaultDimValueGetter(itemOpt, dimName, dataIndex, dimIndex); } : null; list.hasItemOption = false; list.initData(source, null, dimValueGetter); return list; } function isNeedCompleteOrdinalData(source) { if (source.sourceFormat === SOURCE_FORMAT_ORIGINAL) { var sampleItem = firstDataNotNull(source.data || []); return sampleItem != null && !isArray(getDataItemValue(sampleItem)); } } function firstDataNotNull(data) { var i = 0; while (i < data.length && data[i] == null) { i++; } return data[i]; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * // Scale class management * @module echarts/scale/Scale */ /** * @param {Object} [setting] */ function Scale(setting) { this._setting = setting || {}; /** * Extent * @type {Array.} * @protected */ this._extent = [Infinity, -Infinity]; /** * Step is calculated in adjustExtent * @type {Array.} * @protected */ this._interval = 0; this.init && this.init.apply(this, arguments); } /** * Parse input val to valid inner number. * @param {*} val * @return {number} */ Scale.prototype.parse = function (val) { // Notice: This would be a trap here, If the implementation // of this method depends on extent, and this method is used // before extent set (like in dataZoom), it would be wrong. // Nevertheless, parse does not depend on extent generally. return val; }; Scale.prototype.getSetting = function (name) { return this._setting[name]; }; Scale.prototype.contain = function (val) { var extent = this._extent; return val >= extent[0] && val <= extent[1]; }; /** * Normalize value to linear [0, 1], return 0.5 if extent span is 0 * @param {number} val * @return {number} */ Scale.prototype.normalize = function (val) { var extent = this._extent; if (extent[1] === extent[0]) { return 0.5; } return (val - extent[0]) / (extent[1] - extent[0]); }; /** * Scale normalized value * @param {number} val * @return {number} */ Scale.prototype.scale = function (val) { var extent = this._extent; return val * (extent[1] - extent[0]) + extent[0]; }; /** * Set extent from data * @param {Array.} other */ Scale.prototype.unionExtent = function (other) { var extent = this._extent; other[0] < extent[0] && (extent[0] = other[0]); other[1] > extent[1] && (extent[1] = other[1]); // not setExtent because in log axis it may transformed to power // this.setExtent(extent[0], extent[1]); }; /** * Set extent from data * @param {module:echarts/data/List} data * @param {string} dim */ Scale.prototype.unionExtentFromData = function (data, dim) { this.unionExtent(data.getApproximateExtent(dim)); }; /** * Get extent * @return {Array.} */ Scale.prototype.getExtent = function () { return this._extent.slice(); }; /** * Set extent * @param {number} start * @param {number} end */ Scale.prototype.setExtent = function (start, end) { var thisExtent = this._extent; if (!isNaN(start)) { thisExtent[0] = start; } if (!isNaN(end)) { thisExtent[1] = end; } }; /** * When axis extent depends on data and no data exists, * axis ticks should not be drawn, which is named 'blank'. */ Scale.prototype.isBlank = function () { return this._isBlank; }, /** * When axis extent depends on data and no data exists, * axis ticks should not be drawn, which is named 'blank'. */ Scale.prototype.setBlank = function (isBlank) { this._isBlank = isBlank; }; /** * @abstract * @param {*} tick * @return {string} label of the tick. */ Scale.prototype.getLabel = null; enableClassExtend(Scale); enableClassManagement(Scale, { registerWhenExtend: true }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @constructor * @param {Object} [opt] * @param {Object} [opt.categories=[]] * @param {Object} [opt.needCollect=false] * @param {Object} [opt.deduplication=false] */ function OrdinalMeta(opt) { /** * @readOnly * @type {Array.} */ this.categories = opt.categories || []; /** * @private * @type {boolean} */ this._needCollect = opt.needCollect; /** * @private * @type {boolean} */ this._deduplication = opt.deduplication; /** * @private * @type {boolean} */ this._map; } /** * @param {module:echarts/model/Model} axisModel * @return {module:echarts/data/OrdinalMeta} */ OrdinalMeta.createByAxisModel = function (axisModel) { var option = axisModel.option; var data = option.data; var categories = data && map(data, getName); return new OrdinalMeta({ categories: categories, needCollect: !categories, // deduplication is default in axis. deduplication: option.dedplication !== false }); }; var proto$1 = OrdinalMeta.prototype; /** * @param {string} category * @return {number} ordinal */ proto$1.getOrdinal = function (category) { return getOrCreateMap(this).get(category); }; /** * @param {*} category * @return {number} The ordinal. If not found, return NaN. */ proto$1.parseAndCollect = function (category) { var index; var needCollect = this._needCollect; // The value of category dim can be the index of the given category set. // This feature is only supported when !needCollect, because we should // consider a common case: a value is 2017, which is a number but is // expected to be tread as a category. This case usually happen in dataset, // where it happent to be no need of the index feature. if (typeof category !== 'string' && !needCollect) { return category; } // Optimize for the scenario: // category is ['2012-01-01', '2012-01-02', ...], where the input // data has been ensured not duplicate and is large data. // Notice, if a dataset dimension provide categroies, usually echarts // should remove duplication except user tell echarts dont do that // (set axis.deduplication = false), because echarts do not know whether // the values in the category dimension has duplication (consider the // parallel-aqi example) if (needCollect && !this._deduplication) { index = this.categories.length; this.categories[index] = category; return index; } var map$$1 = getOrCreateMap(this); index = map$$1.get(category); if (index == null) { if (needCollect) { index = this.categories.length; this.categories[index] = category; map$$1.set(category, index); } else { index = NaN; } } return index; }; // Consider big data, do not create map until needed. function getOrCreateMap(ordinalMeta) { return ordinalMeta._map || ( ordinalMeta._map = createHashMap(ordinalMeta.categories) ); } function getName(obj) { if (isObject$1(obj) && obj.value != null) { return obj.value; } else { return obj + ''; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Linear continuous scale * @module echarts/coord/scale/Ordinal * * http://en.wikipedia.org/wiki/Level_of_measurement */ // FIXME only one data var scaleProto = Scale.prototype; var OrdinalScale = Scale.extend({ type: 'ordinal', /** * @param {module:echarts/data/OrdianlMeta|Array.} ordinalMeta */ init: function (ordinalMeta, extent) { // Caution: Should not use instanceof, consider ec-extensions using // import approach to get OrdinalMeta class. if (!ordinalMeta || isArray(ordinalMeta)) { ordinalMeta = new OrdinalMeta({categories: ordinalMeta}); } this._ordinalMeta = ordinalMeta; this._extent = extent || [0, ordinalMeta.categories.length - 1]; }, parse: function (val) { return typeof val === 'string' ? this._ordinalMeta.getOrdinal(val) // val might be float. : Math.round(val); }, contain: function (rank) { rank = this.parse(rank); return scaleProto.contain.call(this, rank) && this._ordinalMeta.categories[rank] != null; }, /** * Normalize given rank or name to linear [0, 1] * @param {number|string} [val] * @return {number} */ normalize: function (val) { return scaleProto.normalize.call(this, this.parse(val)); }, scale: function (val) { return Math.round(scaleProto.scale.call(this, val)); }, /** * @return {Array} */ getTicks: function () { var ticks = []; var extent = this._extent; var rank = extent[0]; while (rank <= extent[1]) { ticks.push(rank); rank++; } return ticks; }, /** * Get item on rank n * @param {number} n * @return {string} */ getLabel: function (n) { if (!this.isBlank()) { // Note that if no data, ordinalMeta.categories is an empty array. return this._ordinalMeta.categories[n]; } }, /** * @return {number} */ count: function () { return this._extent[1] - this._extent[0] + 1; }, /** * @override */ unionExtentFromData: function (data, dim) { this.unionExtent(data.getApproximateExtent(dim)); }, getOrdinalMeta: function () { return this._ordinalMeta; }, niceTicks: noop, niceExtent: noop }); /** * @return {module:echarts/scale/Time} */ OrdinalScale.create = function () { return new OrdinalScale(); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * For testable. */ var roundNumber$1 = round$1; /** * @param {Array.} extent Both extent[0] and extent[1] should be valid number. * Should be extent[0] < extent[1]. * @param {number} splitNumber splitNumber should be >= 1. * @param {number} [minInterval] * @param {number} [maxInterval] * @return {Object} {interval, intervalPrecision, niceTickExtent} */ function intervalScaleNiceTicks(extent, splitNumber, minInterval, maxInterval) { var result = {}; var span = extent[1] - extent[0]; var interval = result.interval = nice(span / splitNumber, true); if (minInterval != null && interval < minInterval) { interval = result.interval = minInterval; } if (maxInterval != null && interval > maxInterval) { interval = result.interval = maxInterval; } // Tow more digital for tick. var precision = result.intervalPrecision = getIntervalPrecision(interval); // Niced extent inside original extent var niceTickExtent = result.niceTickExtent = [ roundNumber$1(Math.ceil(extent[0] / interval) * interval, precision), roundNumber$1(Math.floor(extent[1] / interval) * interval, precision) ]; fixExtent(niceTickExtent, extent); return result; } /** * @param {number} interval * @return {number} interval precision */ function getIntervalPrecision(interval) { // Tow more digital for tick. return getPrecisionSafe(interval) + 2; } function clamp(niceTickExtent, idx, extent) { niceTickExtent[idx] = Math.max(Math.min(niceTickExtent[idx], extent[1]), extent[0]); } // In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent. function fixExtent(niceTickExtent, extent) { !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]); !isFinite(niceTickExtent[1]) && (niceTickExtent[1] = extent[1]); clamp(niceTickExtent, 0, extent); clamp(niceTickExtent, 1, extent); if (niceTickExtent[0] > niceTickExtent[1]) { niceTickExtent[0] = niceTickExtent[1]; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Interval scale * @module echarts/scale/Interval */ var roundNumber = round$1; /** * @alias module:echarts/coord/scale/Interval * @constructor */ var IntervalScale = Scale.extend({ type: 'interval', _interval: 0, _intervalPrecision: 2, setExtent: function (start, end) { var thisExtent = this._extent; //start,end may be a Number like '25',so... if (!isNaN(start)) { thisExtent[0] = parseFloat(start); } if (!isNaN(end)) { thisExtent[1] = parseFloat(end); } }, unionExtent: function (other) { var extent = this._extent; other[0] < extent[0] && (extent[0] = other[0]); other[1] > extent[1] && (extent[1] = other[1]); // unionExtent may called by it's sub classes IntervalScale.prototype.setExtent.call(this, extent[0], extent[1]); }, /** * Get interval */ getInterval: function () { return this._interval; }, /** * Set interval */ setInterval: function (interval) { this._interval = interval; // Dropped auto calculated niceExtent and use user setted extent // We assume user wan't to set both interval, min, max to get a better result this._niceExtent = this._extent.slice(); this._intervalPrecision = getIntervalPrecision(interval); }, /** * @param {boolean} [expandToNicedExtent=false] If expand the ticks to niced extent. * @return {Array.} */ getTicks: function (expandToNicedExtent) { var interval = this._interval; var extent = this._extent; var niceTickExtent = this._niceExtent; var intervalPrecision = this._intervalPrecision; var ticks = []; // If interval is 0, return []; if (!interval) { return ticks; } // Consider this case: using dataZoom toolbox, zoom and zoom. var safeLimit = 10000; if (extent[0] < niceTickExtent[0]) { if (expandToNicedExtent) { ticks.push(roundNumber(niceTickExtent[0] - interval, intervalPrecision)); } else { ticks.push(extent[0]); } } var tick = niceTickExtent[0]; while (tick <= niceTickExtent[1]) { ticks.push(tick); // Avoid rounding error tick = roundNumber(tick + interval, intervalPrecision); if (tick === ticks[ticks.length - 1]) { // Consider out of safe float point, e.g., // -3711126.9907707 + 2e-10 === -3711126.9907707 break; } if (ticks.length > safeLimit) { return []; } } // Consider this case: the last item of ticks is smaller // than niceTickExtent[1] and niceTickExtent[1] === extent[1]. var lastNiceTick = ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1]; if (extent[1] > lastNiceTick) { if (expandToNicedExtent) { ticks.push(roundNumber(lastNiceTick + interval, intervalPrecision)); } else { ticks.push(extent[1]); } } return ticks; }, /** * @param {number} [splitNumber=5] * @return {Array.>} */ getMinorTicks: function (splitNumber) { var ticks = this.getTicks(true); var minorTicks = []; var extent = this.getExtent(); for (var i = 1; i < ticks.length; i++) { var nextTick = ticks[i]; var prevTick = ticks[i - 1]; var count = 0; var minorTicksGroup = []; var interval = nextTick - prevTick; var minorInterval = interval / splitNumber; while (count < splitNumber - 1) { var minorTick = round$1(prevTick + (count + 1) * minorInterval); // For the first and last interval. The count may be less than splitNumber. if (minorTick > extent[0] && minorTick < extent[1]) { minorTicksGroup.push(minorTick); } count++; } minorTicks.push(minorTicksGroup); } return minorTicks; }, /** * @param {number} data * @param {Object} [opt] * @param {number|string} [opt.precision] If 'auto', use nice presision. * @param {boolean} [opt.pad] returns 1.50 but not 1.5 if precision is 2. * @return {string} */ getLabel: function (data, opt) { if (data == null) { return ''; } var precision = opt && opt.precision; if (precision == null) { precision = getPrecisionSafe(data) || 0; } else if (precision === 'auto') { // Should be more precise then tick. precision = this._intervalPrecision; } // (1) If `precision` is set, 12.005 should be display as '12.00500'. // (2) Use roundNumber (toFixed) to avoid scientific notation like '3.5e-7'. data = roundNumber(data, precision, true); return addCommas(data); }, /** * Update interval and extent of intervals for nice ticks * * @param {number} [splitNumber = 5] Desired number of ticks * @param {number} [minInterval] * @param {number} [maxInterval] */ niceTicks: function (splitNumber, minInterval, maxInterval) { splitNumber = splitNumber || 5; var extent = this._extent; var span = extent[1] - extent[0]; if (!isFinite(span)) { return; } // User may set axis min 0 and data are all negative // FIXME If it needs to reverse ? if (span < 0) { span = -span; extent.reverse(); } var result = intervalScaleNiceTicks( extent, splitNumber, minInterval, maxInterval ); this._intervalPrecision = result.intervalPrecision; this._interval = result.interval; this._niceExtent = result.niceTickExtent; }, /** * Nice extent. * @param {Object} opt * @param {number} [opt.splitNumber = 5] Given approx tick number * @param {boolean} [opt.fixMin=false] * @param {boolean} [opt.fixMax=false] * @param {boolean} [opt.minInterval] * @param {boolean} [opt.maxInterval] */ niceExtent: function (opt) { var extent = this._extent; // If extent start and end are same, expand them if (extent[0] === extent[1]) { if (extent[0] !== 0) { // Expand extent var expandSize = extent[0]; // In the fowllowing case // Axis has been fixed max 100 // Plus data are all 100 and axis extent are [100, 100]. // Extend to the both side will cause expanded max is larger than fixed max. // So only expand to the smaller side. if (!opt.fixMax) { extent[1] += expandSize / 2; extent[0] -= expandSize / 2; } else { extent[0] -= expandSize / 2; } } else { extent[1] = 1; } } var span = extent[1] - extent[0]; // If there are no data and extent are [Infinity, -Infinity] if (!isFinite(span)) { extent[0] = 0; extent[1] = 1; } this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); // var extent = this._extent; var interval = this._interval; if (!opt.fixMin) { extent[0] = roundNumber(Math.floor(extent[0] / interval) * interval); } if (!opt.fixMax) { extent[1] = roundNumber(Math.ceil(extent[1] / interval) * interval); } } }); /** * @return {module:echarts/scale/Time} */ IntervalScale.create = function () { return new IntervalScale(); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global Float32Array */ var STACK_PREFIX = '__ec_stack_'; var LARGE_BAR_MIN_WIDTH = 0.5; var LargeArr = typeof Float32Array !== 'undefined' ? Float32Array : Array; function getSeriesStackId(seriesModel) { return seriesModel.get('stack') || STACK_PREFIX + seriesModel.seriesIndex; } function getAxisKey(axis) { return axis.dim + axis.index; } /** * @param {Object} opt * @param {module:echarts/coord/Axis} opt.axis Only support category axis currently. * @param {number} opt.count Positive interger. * @param {number} [opt.barWidth] * @param {number} [opt.barMaxWidth] * @param {number} [opt.barMinWidth] * @param {number} [opt.barGap] * @param {number} [opt.barCategoryGap] * @return {Object} {width, offset, offsetCenter} If axis.type is not 'category', return undefined. */ function prepareLayoutBarSeries(seriesType, ecModel) { var seriesModels = []; ecModel.eachSeriesByType(seriesType, function (seriesModel) { // Check series coordinate, do layout for cartesian2d only if (isOnCartesian(seriesModel) && !isInLargeMode(seriesModel)) { seriesModels.push(seriesModel); } }); return seriesModels; } /** * Map from (baseAxis.dim + '_' + baseAxis.index) to min gap of two adjacent * values. * This works for time axes, value axes, and log axes. * For a single time axis, return value is in the form like * {'x_0': [1000000]}. * The value of 1000000 is in milliseconds. */ function getValueAxesMinGaps(barSeries) { /** * Map from axis.index to values. * For a single time axis, axisValues is in the form like * {'x_0': [1495555200000, 1495641600000, 1495728000000]}. * Items in axisValues[x], e.g. 1495555200000, are time values of all * series. */ var axisValues = {}; each$1(barSeries, function (seriesModel) { var cartesian = seriesModel.coordinateSystem; var baseAxis = cartesian.getBaseAxis(); if (baseAxis.type !== 'time' && baseAxis.type !== 'value') { return; } var data = seriesModel.getData(); var key = baseAxis.dim + '_' + baseAxis.index; var dim = data.mapDimension(baseAxis.dim); for (var i = 0, cnt = data.count(); i < cnt; ++i) { var value = data.get(dim, i); if (!axisValues[key]) { // No previous data for the axis axisValues[key] = [value]; } else { // No value in previous series axisValues[key].push(value); } // Ignore duplicated time values in the same axis } }); var axisMinGaps = []; for (var key in axisValues) { if (axisValues.hasOwnProperty(key)) { var valuesInAxis = axisValues[key]; if (valuesInAxis) { // Sort axis values into ascending order to calculate gaps valuesInAxis.sort(function (a, b) { return a - b; }); var min = null; for (var j = 1; j < valuesInAxis.length; ++j) { var delta = valuesInAxis[j] - valuesInAxis[j - 1]; if (delta > 0) { // Ignore 0 delta because they are of the same axis value min = min === null ? delta : Math.min(min, delta); } } // Set to null if only have one data axisMinGaps[key] = min; } } } return axisMinGaps; } function makeColumnLayout(barSeries) { var axisMinGaps = getValueAxesMinGaps(barSeries); var seriesInfoList = []; each$1(barSeries, function (seriesModel) { var cartesian = seriesModel.coordinateSystem; var baseAxis = cartesian.getBaseAxis(); var axisExtent = baseAxis.getExtent(); var bandWidth; if (baseAxis.type === 'category') { bandWidth = baseAxis.getBandWidth(); } else if (baseAxis.type === 'value' || baseAxis.type === 'time') { var key = baseAxis.dim + '_' + baseAxis.index; var minGap = axisMinGaps[key]; var extentSpan = Math.abs(axisExtent[1] - axisExtent[0]); var scale = baseAxis.scale.getExtent(); var scaleSpan = Math.abs(scale[1] - scale[0]); bandWidth = minGap ? extentSpan / scaleSpan * minGap : extentSpan; // When there is only one data value } else { var data = seriesModel.getData(); bandWidth = Math.abs(axisExtent[1] - axisExtent[0]) / data.count(); } var barWidth = parsePercent$1( seriesModel.get('barWidth'), bandWidth ); var barMaxWidth = parsePercent$1( seriesModel.get('barMaxWidth'), bandWidth ); var barMinWidth = parsePercent$1( // barMinWidth by default is 1 in cartesian. Because in value axis, // the auto-calculated bar width might be less than 1. seriesModel.get('barMinWidth') || 1, bandWidth ); var barGap = seriesModel.get('barGap'); var barCategoryGap = seriesModel.get('barCategoryGap'); seriesInfoList.push({ bandWidth: bandWidth, barWidth: barWidth, barMaxWidth: barMaxWidth, barMinWidth: barMinWidth, barGap: barGap, barCategoryGap: barCategoryGap, axisKey: getAxisKey(baseAxis), stackId: getSeriesStackId(seriesModel) }); }); return doCalBarWidthAndOffset(seriesInfoList); } function doCalBarWidthAndOffset(seriesInfoList) { // Columns info on each category axis. Key is cartesian name var columnsMap = {}; each$1(seriesInfoList, function (seriesInfo, idx) { var axisKey = seriesInfo.axisKey; var bandWidth = seriesInfo.bandWidth; var columnsOnAxis = columnsMap[axisKey] || { bandWidth: bandWidth, remainedWidth: bandWidth, autoWidthCount: 0, categoryGap: '20%', gap: '30%', stacks: {} }; var stacks = columnsOnAxis.stacks; columnsMap[axisKey] = columnsOnAxis; var stackId = seriesInfo.stackId; if (!stacks[stackId]) { columnsOnAxis.autoWidthCount++; } stacks[stackId] = stacks[stackId] || { width: 0, maxWidth: 0 }; // Caution: In a single coordinate system, these barGrid attributes // will be shared by series. Consider that they have default values, // only the attributes set on the last series will work. // Do not change this fact unless there will be a break change. var barWidth = seriesInfo.barWidth; if (barWidth && !stacks[stackId].width) { // See #6312, do not restrict width. stacks[stackId].width = barWidth; barWidth = Math.min(columnsOnAxis.remainedWidth, barWidth); columnsOnAxis.remainedWidth -= barWidth; } var barMaxWidth = seriesInfo.barMaxWidth; barMaxWidth && (stacks[stackId].maxWidth = barMaxWidth); var barMinWidth = seriesInfo.barMinWidth; barMinWidth && (stacks[stackId].minWidth = barMinWidth); var barGap = seriesInfo.barGap; (barGap != null) && (columnsOnAxis.gap = barGap); var barCategoryGap = seriesInfo.barCategoryGap; (barCategoryGap != null) && (columnsOnAxis.categoryGap = barCategoryGap); }); var result = {}; each$1(columnsMap, function (columnsOnAxis, coordSysName) { result[coordSysName] = {}; var stacks = columnsOnAxis.stacks; var bandWidth = columnsOnAxis.bandWidth; var categoryGap = parsePercent$1(columnsOnAxis.categoryGap, bandWidth); var barGapPercent = parsePercent$1(columnsOnAxis.gap, 1); var remainedWidth = columnsOnAxis.remainedWidth; var autoWidthCount = columnsOnAxis.autoWidthCount; var autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent); autoWidth = Math.max(autoWidth, 0); // Find if any auto calculated bar exceeded maxBarWidth each$1(stacks, function (column) { var maxWidth = column.maxWidth; var minWidth = column.minWidth; if (!column.width) { var finalWidth = autoWidth; if (maxWidth && maxWidth < finalWidth) { finalWidth = Math.min(maxWidth, remainedWidth); } // `minWidth` has higher priority. `minWidth` decide that wheter the // bar is able to be visible. So `minWidth` should not be restricted // by `maxWidth` or `remainedWidth` (which is from `bandWidth`). In // the extreme cases for `value` axis, bars are allowed to overlap // with each other if `minWidth` specified. if (minWidth && minWidth > finalWidth) { finalWidth = minWidth; } if (finalWidth !== autoWidth) { column.width = finalWidth; remainedWidth -= finalWidth + barGapPercent * finalWidth; autoWidthCount--; } } else { // `barMinWidth/barMaxWidth` has higher priority than `barWidth`, as // CSS does. Becuase barWidth can be a percent value, where // `barMaxWidth` can be used to restrict the final width. var finalWidth = column.width; if (maxWidth) { finalWidth = Math.min(finalWidth, maxWidth); } // `minWidth` has higher priority, as described above if (minWidth) { finalWidth = Math.max(finalWidth, minWidth); } column.width = finalWidth; remainedWidth -= finalWidth + barGapPercent * finalWidth; autoWidthCount--; } }); // Recalculate width again autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent); autoWidth = Math.max(autoWidth, 0); var widthSum = 0; var lastColumn; each$1(stacks, function (column, idx) { if (!column.width) { column.width = autoWidth; } lastColumn = column; widthSum += column.width * (1 + barGapPercent); }); if (lastColumn) { widthSum -= lastColumn.width * barGapPercent; } var offset = -widthSum / 2; each$1(stacks, function (column, stackId) { result[coordSysName][stackId] = result[coordSysName][stackId] || { bandWidth: bandWidth, offset: offset, width: column.width }; offset += column.width * (1 + barGapPercent); }); }); return result; } /** * @param {Object} barWidthAndOffset The result of makeColumnLayout * @param {module:echarts/coord/Axis} axis * @param {module:echarts/model/Series} [seriesModel] If not provided, return all. * @return {Object} {stackId: {offset, width}} or {offset, width} if seriesModel provided. */ function retrieveColumnLayout(barWidthAndOffset, axis, seriesModel) { if (barWidthAndOffset && axis) { var result = barWidthAndOffset[getAxisKey(axis)]; if (result != null && seriesModel != null) { result = result[getSeriesStackId(seriesModel)]; } return result; } } /** * @param {string} seriesType * @param {module:echarts/model/Global} ecModel */ function layout(seriesType, ecModel) { var seriesModels = prepareLayoutBarSeries(seriesType, ecModel); var barWidthAndOffset = makeColumnLayout(seriesModels); var lastStackCoords = {}; each$1(seriesModels, function (seriesModel) { var data = seriesModel.getData(); var cartesian = seriesModel.coordinateSystem; var baseAxis = cartesian.getBaseAxis(); var stackId = getSeriesStackId(seriesModel); var columnLayoutInfo = barWidthAndOffset[getAxisKey(baseAxis)][stackId]; var columnOffset = columnLayoutInfo.offset; var columnWidth = columnLayoutInfo.width; var valueAxis = cartesian.getOtherAxis(baseAxis); var barMinHeight = seriesModel.get('barMinHeight') || 0; lastStackCoords[stackId] = lastStackCoords[stackId] || []; data.setLayout({ bandWidth: columnLayoutInfo.bandWidth, offset: columnOffset, size: columnWidth }); var valueDim = data.mapDimension(valueAxis.dim); var baseDim = data.mapDimension(baseAxis.dim); var stacked = isDimensionStacked(data, valueDim /*, baseDim*/); var isValueAxisH = valueAxis.isHorizontal(); var valueAxisStart = getValueAxisStart(baseAxis, valueAxis, stacked); for (var idx = 0, len = data.count(); idx < len; idx++) { var value = data.get(valueDim, idx); var baseValue = data.get(baseDim, idx); var sign = value >= 0 ? 'p' : 'n'; var baseCoord = valueAxisStart; // Because of the barMinHeight, we can not use the value in // stackResultDimension directly. if (stacked) { // Only ordinal axis can be stacked. if (!lastStackCoords[stackId][baseValue]) { lastStackCoords[stackId][baseValue] = { p: valueAxisStart, // Positive stack n: valueAxisStart // Negative stack }; } // Should also consider #4243 baseCoord = lastStackCoords[stackId][baseValue][sign]; } var x; var y; var width; var height; if (isValueAxisH) { var coord = cartesian.dataToPoint([value, baseValue]); x = baseCoord; y = coord[1] + columnOffset; width = coord[0] - valueAxisStart; height = columnWidth; if (Math.abs(width) < barMinHeight) { width = (width < 0 ? -1 : 1) * barMinHeight; } // Ignore stack from NaN value if (!isNaN(width)) { stacked && (lastStackCoords[stackId][baseValue][sign] += width); } } else { var coord = cartesian.dataToPoint([baseValue, value]); x = coord[0] + columnOffset; y = baseCoord; width = columnWidth; height = coord[1] - valueAxisStart; if (Math.abs(height) < barMinHeight) { // Include zero to has a positive bar height = (height <= 0 ? -1 : 1) * barMinHeight; } // Ignore stack from NaN value if (!isNaN(height)) { stacked && (lastStackCoords[stackId][baseValue][sign] += height); } } data.setItemLayout(idx, { x: x, y: y, width: width, height: height }); } }, this); } // TODO: Do not support stack in large mode yet. var largeLayout = { seriesType: 'bar', plan: createRenderPlanner(), reset: function (seriesModel) { if (!isOnCartesian(seriesModel) || !isInLargeMode(seriesModel)) { return; } var data = seriesModel.getData(); var cartesian = seriesModel.coordinateSystem; var coordLayout = cartesian.grid.getRect(); var baseAxis = cartesian.getBaseAxis(); var valueAxis = cartesian.getOtherAxis(baseAxis); var valueDim = data.mapDimension(valueAxis.dim); var baseDim = data.mapDimension(baseAxis.dim); var valueAxisHorizontal = valueAxis.isHorizontal(); var valueDimIdx = valueAxisHorizontal ? 0 : 1; var barWidth = retrieveColumnLayout( makeColumnLayout([seriesModel]), baseAxis, seriesModel ).width; if (!(barWidth > LARGE_BAR_MIN_WIDTH)) { // jshint ignore:line barWidth = LARGE_BAR_MIN_WIDTH; } return {progress: progress}; function progress(params, data) { var count = params.count; var largePoints = new LargeArr(count * 2); var largeBackgroundPoints = new LargeArr(count * 2); var largeDataIndices = new LargeArr(count); var dataIndex; var coord = []; var valuePair = []; var pointsOffset = 0; var idxOffset = 0; while ((dataIndex = params.next()) != null) { valuePair[valueDimIdx] = data.get(valueDim, dataIndex); valuePair[1 - valueDimIdx] = data.get(baseDim, dataIndex); coord = cartesian.dataToPoint(valuePair, null, coord); // Data index might not be in order, depends on `progressiveChunkMode`. largeBackgroundPoints[pointsOffset] = valueAxisHorizontal ? coordLayout.x + coordLayout.width : coord[0]; largePoints[pointsOffset++] = coord[0]; largeBackgroundPoints[pointsOffset] = valueAxisHorizontal ? coord[1] : coordLayout.y + coordLayout.height; largePoints[pointsOffset++] = coord[1]; largeDataIndices[idxOffset++] = dataIndex; } data.setLayout({ largePoints: largePoints, largeDataIndices: largeDataIndices, largeBackgroundPoints: largeBackgroundPoints, barWidth: barWidth, valueAxisStart: getValueAxisStart(baseAxis, valueAxis, false), backgroundStart: valueAxisHorizontal ? coordLayout.x : coordLayout.y, valueAxisHorizontal: valueAxisHorizontal }); } } }; function isOnCartesian(seriesModel) { return seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian2d'; } function isInLargeMode(seriesModel) { return seriesModel.pipelineContext && seriesModel.pipelineContext.large; } // See cases in `test/bar-start.html` and `#7412`, `#8747`. function getValueAxisStart(baseAxis, valueAxis, stacked) { return valueAxis.toGlobalCoord(valueAxis.dataToCoord(valueAxis.type === 'log' ? 1 : 0)); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * A third-party license is embeded for some of the code in this file: * The "scaleLevels" was originally copied from "d3.js" with some * modifications made for this project. * (See more details in the comment on the definition of "scaleLevels" below.) * The use of the source code of this file is also subject to the terms * and consitions of the license of "d3.js" (BSD-3Clause, see * ). */ // [About UTC and local time zone]: // In most cases, `number.parseDate` will treat input data string as local time // (except time zone is specified in time string). And `format.formateTime` returns // local time by default. option.useUTC is false by default. This design have // concidered these common case: // (1) Time that is persistent in server is in UTC, but it is needed to be diplayed // in local time by default. // (2) By default, the input data string (e.g., '2011-01-02') should be displayed // as its original time, without any time difference. var intervalScaleProto = IntervalScale.prototype; var mathCeil = Math.ceil; var mathFloor = Math.floor; var ONE_SECOND = 1000; var ONE_MINUTE = ONE_SECOND * 60; var ONE_HOUR = ONE_MINUTE * 60; var ONE_DAY = ONE_HOUR * 24; // FIXME 公用? var bisect = function (a, x, lo, hi) { while (lo < hi) { var mid = lo + hi >>> 1; if (a[mid][1] < x) { lo = mid + 1; } else { hi = mid; } } return lo; }; /** * @alias module:echarts/coord/scale/Time * @constructor */ var TimeScale = IntervalScale.extend({ type: 'time', /** * @override */ getLabel: function (val) { var stepLvl = this._stepLvl; var date = new Date(val); return formatTime(stepLvl[0], date, this.getSetting('useUTC')); }, /** * @override */ niceExtent: function (opt) { var extent = this._extent; // If extent start and end are same, expand them if (extent[0] === extent[1]) { // Expand extent extent[0] -= ONE_DAY; extent[1] += ONE_DAY; } // If there are no data and extent are [Infinity, -Infinity] if (extent[1] === -Infinity && extent[0] === Infinity) { var d = new Date(); extent[1] = +new Date(d.getFullYear(), d.getMonth(), d.getDate()); extent[0] = extent[1] - ONE_DAY; } this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); // var extent = this._extent; var interval = this._interval; if (!opt.fixMin) { extent[0] = round$1(mathFloor(extent[0] / interval) * interval); } if (!opt.fixMax) { extent[1] = round$1(mathCeil(extent[1] / interval) * interval); } }, /** * @override */ niceTicks: function (approxTickNum, minInterval, maxInterval) { approxTickNum = approxTickNum || 10; var extent = this._extent; var span = extent[1] - extent[0]; var approxInterval = span / approxTickNum; if (minInterval != null && approxInterval < minInterval) { approxInterval = minInterval; } if (maxInterval != null && approxInterval > maxInterval) { approxInterval = maxInterval; } var scaleLevelsLen = scaleLevels.length; var idx = bisect(scaleLevels, approxInterval, 0, scaleLevelsLen); var level = scaleLevels[Math.min(idx, scaleLevelsLen - 1)]; var interval = level[1]; // Same with interval scale if span is much larger than 1 year if (level[0] === 'year') { var yearSpan = span / interval; // From "Nice Numbers for Graph Labels" of Graphic Gems // var niceYearSpan = numberUtil.nice(yearSpan, false); var yearStep = nice(yearSpan / approxTickNum, true); interval *= yearStep; } var timezoneOffset = this.getSetting('useUTC') ? 0 : (new Date(+extent[0] || +extent[1])).getTimezoneOffset() * 60 * 1000; var niceExtent = [ Math.round(mathCeil((extent[0] - timezoneOffset) / interval) * interval + timezoneOffset), Math.round(mathFloor((extent[1] - timezoneOffset) / interval) * interval + timezoneOffset) ]; fixExtent(niceExtent, extent); this._stepLvl = level; // Interval will be used in getTicks this._interval = interval; this._niceExtent = niceExtent; }, parse: function (val) { // val might be float. return +parseDate(val); } }); each$1(['contain', 'normalize'], function (methodName) { TimeScale.prototype[methodName] = function (val) { return intervalScaleProto[methodName].call(this, this.parse(val)); }; }); /** * This implementation was originally copied from "d3.js" * * with some modifications made for this program. * See the license statement at the head of this file. */ var scaleLevels = [ // Format interval ['hh:mm:ss', ONE_SECOND], // 1s ['hh:mm:ss', ONE_SECOND * 5], // 5s ['hh:mm:ss', ONE_SECOND * 10], // 10s ['hh:mm:ss', ONE_SECOND * 15], // 15s ['hh:mm:ss', ONE_SECOND * 30], // 30s ['hh:mm\nMM-dd', ONE_MINUTE], // 1m ['hh:mm\nMM-dd', ONE_MINUTE * 5], // 5m ['hh:mm\nMM-dd', ONE_MINUTE * 10], // 10m ['hh:mm\nMM-dd', ONE_MINUTE * 15], // 15m ['hh:mm\nMM-dd', ONE_MINUTE * 30], // 30m ['hh:mm\nMM-dd', ONE_HOUR], // 1h ['hh:mm\nMM-dd', ONE_HOUR * 2], // 2h ['hh:mm\nMM-dd', ONE_HOUR * 6], // 6h ['hh:mm\nMM-dd', ONE_HOUR * 12], // 12h ['MM-dd\nyyyy', ONE_DAY], // 1d ['MM-dd\nyyyy', ONE_DAY * 2], // 2d ['MM-dd\nyyyy', ONE_DAY * 3], // 3d ['MM-dd\nyyyy', ONE_DAY * 4], // 4d ['MM-dd\nyyyy', ONE_DAY * 5], // 5d ['MM-dd\nyyyy', ONE_DAY * 6], // 6d ['week', ONE_DAY * 7], // 7d ['MM-dd\nyyyy', ONE_DAY * 10], // 10d ['week', ONE_DAY * 14], // 2w ['week', ONE_DAY * 21], // 3w ['month', ONE_DAY * 31], // 1M ['week', ONE_DAY * 42], // 6w ['month', ONE_DAY * 62], // 2M ['week', ONE_DAY * 70], // 10w ['quarter', ONE_DAY * 95], // 3M ['month', ONE_DAY * 31 * 4], // 4M ['month', ONE_DAY * 31 * 5], // 5M ['half-year', ONE_DAY * 380 / 2], // 6M ['month', ONE_DAY * 31 * 8], // 8M ['month', ONE_DAY * 31 * 10], // 10M ['year', ONE_DAY * 380] // 1Y ]; /** * @param {module:echarts/model/Model} * @return {module:echarts/scale/Time} */ TimeScale.create = function (model) { return new TimeScale({useUTC: model.ecModel.get('useUTC')}); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Log scale * @module echarts/scale/Log */ // Use some method of IntervalScale var scaleProto$1 = Scale.prototype; var intervalScaleProto$1 = IntervalScale.prototype; var getPrecisionSafe$1 = getPrecisionSafe; var roundingErrorFix = round$1; var mathFloor$1 = Math.floor; var mathCeil$1 = Math.ceil; var mathPow$1 = Math.pow; var mathLog = Math.log; var LogScale = Scale.extend({ type: 'log', base: 10, $constructor: function () { Scale.apply(this, arguments); this._originalScale = new IntervalScale(); }, /** * @param {boolean} [expandToNicedExtent=false] If expand the ticks to niced extent. * @return {Array.} */ getTicks: function (expandToNicedExtent) { var originalScale = this._originalScale; var extent = this._extent; var originalExtent = originalScale.getExtent(); return map(intervalScaleProto$1.getTicks.call(this, expandToNicedExtent), function (val) { var powVal = round$1(mathPow$1(this.base, val)); // Fix #4158 powVal = (val === extent[0] && originalScale.__fixMin) ? fixRoundingError(powVal, originalExtent[0]) : powVal; powVal = (val === extent[1] && originalScale.__fixMax) ? fixRoundingError(powVal, originalExtent[1]) : powVal; return powVal; }, this); }, /** * @param {number} splitNumber * @return {Array.>} */ getMinorTicks: intervalScaleProto$1.getMinorTicks, /** * @param {number} val * @return {string} */ getLabel: intervalScaleProto$1.getLabel, /** * @param {number} val * @return {number} */ scale: function (val) { val = scaleProto$1.scale.call(this, val); return mathPow$1(this.base, val); }, /** * @param {number} start * @param {number} end */ setExtent: function (start, end) { var base = this.base; start = mathLog(start) / mathLog(base); end = mathLog(end) / mathLog(base); intervalScaleProto$1.setExtent.call(this, start, end); }, /** * @return {number} end */ getExtent: function () { var base = this.base; var extent = scaleProto$1.getExtent.call(this); extent[0] = mathPow$1(base, extent[0]); extent[1] = mathPow$1(base, extent[1]); // Fix #4158 var originalScale = this._originalScale; var originalExtent = originalScale.getExtent(); originalScale.__fixMin && (extent[0] = fixRoundingError(extent[0], originalExtent[0])); originalScale.__fixMax && (extent[1] = fixRoundingError(extent[1], originalExtent[1])); return extent; }, /** * @param {Array.} extent */ unionExtent: function (extent) { this._originalScale.unionExtent(extent); var base = this.base; extent[0] = mathLog(extent[0]) / mathLog(base); extent[1] = mathLog(extent[1]) / mathLog(base); scaleProto$1.unionExtent.call(this, extent); }, /** * @override */ unionExtentFromData: function (data, dim) { // TODO // filter value that <= 0 this.unionExtent(data.getApproximateExtent(dim)); }, /** * Update interval and extent of intervals for nice ticks * @param {number} [approxTickNum = 10] Given approx tick number */ niceTicks: function (approxTickNum) { approxTickNum = approxTickNum || 10; var extent = this._extent; var span = extent[1] - extent[0]; if (span === Infinity || span <= 0) { return; } var interval = quantity(span); var err = approxTickNum / span * interval; // Filter ticks to get closer to the desired count. if (err <= 0.5) { interval *= 10; } // Interval should be integer while (!isNaN(interval) && Math.abs(interval) < 1 && Math.abs(interval) > 0) { interval *= 10; } var niceExtent = [ round$1(mathCeil$1(extent[0] / interval) * interval), round$1(mathFloor$1(extent[1] / interval) * interval) ]; this._interval = interval; this._niceExtent = niceExtent; }, /** * Nice extent. * @override */ niceExtent: function (opt) { intervalScaleProto$1.niceExtent.call(this, opt); var originalScale = this._originalScale; originalScale.__fixMin = opt.fixMin; originalScale.__fixMax = opt.fixMax; } }); each$1(['contain', 'normalize'], function (methodName) { LogScale.prototype[methodName] = function (val) { val = mathLog(val) / mathLog(this.base); return scaleProto$1[methodName].call(this, val); }; }); LogScale.create = function () { return new LogScale(); }; function fixRoundingError(val, originalVal) { return roundingErrorFix(val, getPrecisionSafe$1(originalVal)); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Get axis scale extent before niced. * Item of returned array can only be number (including Infinity and NaN). */ function getScaleExtent(scale, model) { var scaleType = scale.type; var min = model.getMin(); var max = model.getMax(); var originalExtent = scale.getExtent(); var axisDataLen; var boundaryGap; var span; if (scaleType === 'ordinal') { axisDataLen = model.getCategories().length; } else { boundaryGap = model.get('boundaryGap'); if (!isArray(boundaryGap)) { boundaryGap = [boundaryGap || 0, boundaryGap || 0]; } if (typeof boundaryGap[0] === 'boolean') { if (__DEV__) { console.warn('Boolean type for boundaryGap is only ' + 'allowed for ordinal axis. Please use string in ' + 'percentage instead, e.g., "20%". Currently, ' + 'boundaryGap is set to be 0.'); } boundaryGap = [0, 0]; } boundaryGap[0] = parsePercent$1(boundaryGap[0], 1); boundaryGap[1] = parsePercent$1(boundaryGap[1], 1); span = (originalExtent[1] - originalExtent[0]) || Math.abs(originalExtent[0]); } // Notice: When min/max is not set (that is, when there are null/undefined, // which is the most common case), these cases should be ensured: // (1) For 'ordinal', show all axis.data. // (2) For others: // + `boundaryGap` is applied (if min/max set, boundaryGap is // disabled). // + If `needCrossZero`, min/max should be zero, otherwise, min/max should // be the result that originalExtent enlarged by boundaryGap. // (3) If no data, it should be ensured that `scale.setBlank` is set. // FIXME // (1) When min/max is 'dataMin' or 'dataMax', should boundaryGap be able to used? // (2) When `needCrossZero` and all data is positive/negative, should it be ensured // that the results processed by boundaryGap are positive/negative? if (min === 'dataMin') { min = originalExtent[0]; } else if (typeof min === 'function') { min = min({ min: originalExtent[0], max: originalExtent[1] }); } if (max === 'dataMax') { max = originalExtent[1]; } else if (typeof max === 'function') { max = max({ min: originalExtent[0], max: originalExtent[1] }); } var fixMin = min != null; var fixMax = max != null; if (min == null) { min = scaleType === 'ordinal' ? (axisDataLen ? 0 : NaN) : originalExtent[0] - boundaryGap[0] * span; } if (max == null) { max = scaleType === 'ordinal' ? (axisDataLen ? axisDataLen - 1 : NaN) : originalExtent[1] + boundaryGap[1] * span; } (min == null || !isFinite(min)) && (min = NaN); (max == null || !isFinite(max)) && (max = NaN); scale.setBlank( eqNaN(min) || eqNaN(max) || (scaleType === 'ordinal' && !scale.getOrdinalMeta().categories.length) ); // Evaluate if axis needs cross zero if (model.getNeedCrossZero()) { // Axis is over zero and min is not set if (min > 0 && max > 0 && !fixMin) { min = 0; } // Axis is under zero and max is not set if (min < 0 && max < 0 && !fixMax) { max = 0; } } // If bars are placed on a base axis of type time or interval account for axis boundary overflow and current axis // is base axis // FIXME // (1) Consider support value axis, where below zero and axis `onZero` should be handled properly. // (2) Refactor the logic with `barGrid`. Is it not need to `makeBarWidthAndOffsetInfo` twice with different extent? // Should not depend on series type `bar`? // (3) Fix that might overlap when using dataZoom. // (4) Consider other chart types using `barGrid`? // See #6728, #4862, `test/bar-overflow-time-plot.html` var ecModel = model.ecModel; if (ecModel && (scaleType === 'time' /*|| scaleType === 'interval' */)) { var barSeriesModels = prepareLayoutBarSeries('bar', ecModel); var isBaseAxisAndHasBarSeries; each$1(barSeriesModels, function (seriesModel) { isBaseAxisAndHasBarSeries |= seriesModel.getBaseAxis() === model.axis; }); if (isBaseAxisAndHasBarSeries) { // Calculate placement of bars on axis var barWidthAndOffset = makeColumnLayout(barSeriesModels); // Adjust axis min and max to account for overflow var adjustedScale = adjustScaleForOverflow(min, max, model, barWidthAndOffset); min = adjustedScale.min; max = adjustedScale.max; } } return { extent: [min, max], // "fix" means "fixed", the value should not be // changed in the subsequent steps. fixMin: fixMin, fixMax: fixMax }; } function adjustScaleForOverflow(min, max, model, barWidthAndOffset) { // Get Axis Length var axisExtent = model.axis.getExtent(); var axisLength = axisExtent[1] - axisExtent[0]; // Get bars on current base axis and calculate min and max overflow var barsOnCurrentAxis = retrieveColumnLayout(barWidthAndOffset, model.axis); if (barsOnCurrentAxis === undefined) { return {min: min, max: max}; } var minOverflow = Infinity; each$1(barsOnCurrentAxis, function (item) { minOverflow = Math.min(item.offset, minOverflow); }); var maxOverflow = -Infinity; each$1(barsOnCurrentAxis, function (item) { maxOverflow = Math.max(item.offset + item.width, maxOverflow); }); minOverflow = Math.abs(minOverflow); maxOverflow = Math.abs(maxOverflow); var totalOverFlow = minOverflow + maxOverflow; // Calulate required buffer based on old range and overflow var oldRange = max - min; var oldRangePercentOfNew = (1 - (minOverflow + maxOverflow) / axisLength); var overflowBuffer = ((oldRange / oldRangePercentOfNew) - oldRange); max += overflowBuffer * (maxOverflow / totalOverFlow); min -= overflowBuffer * (minOverflow / totalOverFlow); return {min: min, max: max}; } function niceScaleExtent(scale, model) { var extentInfo = getScaleExtent(scale, model); var extent = extentInfo.extent; var splitNumber = model.get('splitNumber'); if (scale.type === 'log') { scale.base = model.get('logBase'); } var scaleType = scale.type; scale.setExtent(extent[0], extent[1]); scale.niceExtent({ splitNumber: splitNumber, fixMin: extentInfo.fixMin, fixMax: extentInfo.fixMax, minInterval: (scaleType === 'interval' || scaleType === 'time') ? model.get('minInterval') : null, maxInterval: (scaleType === 'interval' || scaleType === 'time') ? model.get('maxInterval') : null }); // If some one specified the min, max. And the default calculated interval // is not good enough. He can specify the interval. It is often appeared // in angle axis with angle 0 - 360. Interval calculated in interval scale is hard // to be 60. // FIXME var interval = model.get('interval'); if (interval != null) { scale.setInterval && scale.setInterval(interval); } } /** * @param {module:echarts/model/Model} model * @param {string} [axisType] Default retrieve from model.type * @return {module:echarts/scale/*} */ function createScaleByModel(model, axisType) { axisType = axisType || model.get('type'); if (axisType) { switch (axisType) { // Buildin scale case 'category': return new OrdinalScale( model.getOrdinalMeta ? model.getOrdinalMeta() : model.getCategories(), [Infinity, -Infinity] ); case 'value': return new IntervalScale(); // Extended scale, like time and log default: return (Scale.getClass(axisType) || IntervalScale).create(model); } } } /** * Check if the axis corss 0 */ function ifAxisCrossZero(axis) { var dataExtent = axis.scale.getExtent(); var min = dataExtent[0]; var max = dataExtent[1]; return !((min > 0 && max > 0) || (min < 0 && max < 0)); } /** * @param {module:echarts/coord/Axis} axis * @return {Function} Label formatter function. * param: {number} tickValue, * param: {number} idx, the index in all ticks. * If category axis, this param is not requied. * return: {string} label string. */ function makeLabelFormatter(axis) { var labelFormatter = axis.getLabelModel().get('formatter'); var categoryTickStart = axis.type === 'category' ? axis.scale.getExtent()[0] : null; if (typeof labelFormatter === 'string') { labelFormatter = (function (tpl) { return function (val) { // For category axis, get raw value; for numeric axis, // get foramtted label like '1,333,444'. val = axis.scale.getLabel(val); return tpl.replace('{value}', val != null ? val : ''); }; })(labelFormatter); // Consider empty array return labelFormatter; } else if (typeof labelFormatter === 'function') { return function (tickValue, idx) { // The original intention of `idx` is "the index of the tick in all ticks". // But the previous implementation of category axis do not consider the // `axisLabel.interval`, which cause that, for example, the `interval` is // `1`, then the ticks "name5", "name7", "name9" are displayed, where the // corresponding `idx` are `0`, `2`, `4`, but not `0`, `1`, `2`. So we keep // the definition here for back compatibility. if (categoryTickStart != null) { idx = tickValue - categoryTickStart; } return labelFormatter(getAxisRawValue(axis, tickValue), idx); }; } else { return function (tick) { return axis.scale.getLabel(tick); }; } } function getAxisRawValue(axis, value) { // In category axis with data zoom, tick is not the original // index of axis.data. So tick should not be exposed to user // in category axis. return axis.type === 'category' ? axis.scale.getLabel(value) : value; } /** * @param {module:echarts/coord/Axis} axis * @return {module:zrender/core/BoundingRect} Be null/undefined if no labels. */ function estimateLabelUnionRect(axis) { var axisModel = axis.model; var scale = axis.scale; if (!axisModel.get('axisLabel.show') || scale.isBlank()) { return; } var isCategory = axis.type === 'category'; var realNumberScaleTicks; var tickCount; var categoryScaleExtent = scale.getExtent(); // Optimize for large category data, avoid call `getTicks()`. if (isCategory) { tickCount = scale.count(); } else { realNumberScaleTicks = scale.getTicks(); tickCount = realNumberScaleTicks.length; } var axisLabelModel = axis.getLabelModel(); var labelFormatter = makeLabelFormatter(axis); var rect; var step = 1; // Simple optimization for large amount of labels if (tickCount > 40) { step = Math.ceil(tickCount / 40); } for (var i = 0; i < tickCount; i += step) { var tickValue = realNumberScaleTicks ? realNumberScaleTicks[i] : categoryScaleExtent[0] + i; var label = labelFormatter(tickValue); var unrotatedSingleRect = axisLabelModel.getTextRect(label); var singleRect = rotateTextRect(unrotatedSingleRect, axisLabelModel.get('rotate') || 0); rect ? rect.union(singleRect) : (rect = singleRect); } return rect; } function rotateTextRect(textRect, rotate) { var rotateRadians = rotate * Math.PI / 180; var boundingBox = textRect.plain(); var beforeWidth = boundingBox.width; var beforeHeight = boundingBox.height; var afterWidth = beforeWidth * Math.abs(Math.cos(rotateRadians)) + Math.abs(beforeHeight * Math.sin(rotateRadians)); var afterHeight = beforeWidth * Math.abs(Math.sin(rotateRadians)) + Math.abs(beforeHeight * Math.cos(rotateRadians)); var rotatedRect = new BoundingRect(boundingBox.x, boundingBox.y, afterWidth, afterHeight); return rotatedRect; } /** * @param {module:echarts/src/model/Model} model axisLabelModel or axisTickModel * @return {number|String} Can be null|'auto'|number|function */ function getOptionCategoryInterval(model) { var interval = model.get('interval'); return interval == null ? 'auto' : interval; } /** * Set `categoryInterval` as 0 implicitly indicates that * show all labels reguardless of overlap. * @param {Object} axis axisModel.axis * @return {boolean} */ function shouldShowAllLabels(axis) { return axis.type === 'category' && getOptionCategoryInterval(axis.getLabelModel()) === 0; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import * as axisHelper from './axisHelper'; var axisModelCommonMixin = { /** * @param {boolean} origin * @return {number|string} min value or 'dataMin' or null/undefined (means auto) or NaN */ getMin: function (origin) { var option = this.option; var min = (!origin && option.rangeStart != null) ? option.rangeStart : option.min; if (this.axis && min != null && min !== 'dataMin' && typeof min !== 'function' && !eqNaN(min) ) { min = this.axis.scale.parse(min); } return min; }, /** * @param {boolean} origin * @return {number|string} max value or 'dataMax' or null/undefined (means auto) or NaN */ getMax: function (origin) { var option = this.option; var max = (!origin && option.rangeEnd != null) ? option.rangeEnd : option.max; if (this.axis && max != null && max !== 'dataMax' && typeof max !== 'function' && !eqNaN(max) ) { max = this.axis.scale.parse(max); } return max; }, /** * @return {boolean} */ getNeedCrossZero: function () { var option = this.option; return (option.rangeStart != null || option.rangeEnd != null) ? false : !option.scale; }, /** * Should be implemented by each axis model if necessary. * @return {module:echarts/model/Component} coordinate system model */ getCoordSysModel: noop, /** * @param {number} rangeStart Can only be finite number or null/undefined or NaN. * @param {number} rangeEnd Can only be finite number or null/undefined or NaN. */ setRange: function (rangeStart, rangeEnd) { this.option.rangeStart = rangeStart; this.option.rangeEnd = rangeEnd; }, /** * Reset range */ resetRange: function () { // rangeStart and rangeEnd is readonly. this.option.rangeStart = this.option.rangeEnd = null; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Symbol factory /** * Triangle shape * @inner */ var Triangle = extendShape({ type: 'triangle', shape: { cx: 0, cy: 0, width: 0, height: 0 }, buildPath: function (path, shape) { var cx = shape.cx; var cy = shape.cy; var width = shape.width / 2; var height = shape.height / 2; path.moveTo(cx, cy - height); path.lineTo(cx + width, cy + height); path.lineTo(cx - width, cy + height); path.closePath(); } }); /** * Diamond shape * @inner */ var Diamond = extendShape({ type: 'diamond', shape: { cx: 0, cy: 0, width: 0, height: 0 }, buildPath: function (path, shape) { var cx = shape.cx; var cy = shape.cy; var width = shape.width / 2; var height = shape.height / 2; path.moveTo(cx, cy - height); path.lineTo(cx + width, cy); path.lineTo(cx, cy + height); path.lineTo(cx - width, cy); path.closePath(); } }); /** * Pin shape * @inner */ var Pin = extendShape({ type: 'pin', shape: { // x, y on the cusp x: 0, y: 0, width: 0, height: 0 }, buildPath: function (path, shape) { var x = shape.x; var y = shape.y; var w = shape.width / 5 * 3; // Height must be larger than width var h = Math.max(w, shape.height); var r = w / 2; // Dist on y with tangent point and circle center var dy = r * r / (h - r); var cy = y - h + r + dy; var angle = Math.asin(dy / r); // Dist on x with tangent point and circle center var dx = Math.cos(angle) * r; var tanX = Math.sin(angle); var tanY = Math.cos(angle); var cpLen = r * 0.6; var cpLen2 = r * 0.7; path.moveTo(x - dx, cy + dy); path.arc( x, cy, r, Math.PI - angle, Math.PI * 2 + angle ); path.bezierCurveTo( x + dx - tanX * cpLen, cy + dy + tanY * cpLen, x, y - cpLen2, x, y ); path.bezierCurveTo( x, y - cpLen2, x - dx + tanX * cpLen, cy + dy + tanY * cpLen, x - dx, cy + dy ); path.closePath(); } }); /** * Arrow shape * @inner */ var Arrow = extendShape({ type: 'arrow', shape: { x: 0, y: 0, width: 0, height: 0 }, buildPath: function (ctx, shape) { var height = shape.height; var width = shape.width; var x = shape.x; var y = shape.y; var dx = width / 3 * 2; ctx.moveTo(x, y); ctx.lineTo(x + dx, y + height); ctx.lineTo(x, y + height / 4 * 3); ctx.lineTo(x - dx, y + height); ctx.lineTo(x, y); ctx.closePath(); } }); /** * Map of path contructors * @type {Object.} */ var symbolCtors = { line: Line, rect: Rect, roundRect: Rect, square: Rect, circle: Circle, diamond: Diamond, pin: Pin, arrow: Arrow, triangle: Triangle }; var symbolShapeMakers = { line: function (x, y, w, h, shape) { // FIXME shape.x1 = x; shape.y1 = y + h / 2; shape.x2 = x + w; shape.y2 = y + h / 2; }, rect: function (x, y, w, h, shape) { shape.x = x; shape.y = y; shape.width = w; shape.height = h; }, roundRect: function (x, y, w, h, shape) { shape.x = x; shape.y = y; shape.width = w; shape.height = h; shape.r = Math.min(w, h) / 4; }, square: function (x, y, w, h, shape) { var size = Math.min(w, h); shape.x = x; shape.y = y; shape.width = size; shape.height = size; }, circle: function (x, y, w, h, shape) { // Put circle in the center of square shape.cx = x + w / 2; shape.cy = y + h / 2; shape.r = Math.min(w, h) / 2; }, diamond: function (x, y, w, h, shape) { shape.cx = x + w / 2; shape.cy = y + h / 2; shape.width = w; shape.height = h; }, pin: function (x, y, w, h, shape) { shape.x = x + w / 2; shape.y = y + h / 2; shape.width = w; shape.height = h; }, arrow: function (x, y, w, h, shape) { shape.x = x + w / 2; shape.y = y + h / 2; shape.width = w; shape.height = h; }, triangle: function (x, y, w, h, shape) { shape.cx = x + w / 2; shape.cy = y + h / 2; shape.width = w; shape.height = h; } }; var symbolBuildProxies = {}; each$1(symbolCtors, function (Ctor, name) { symbolBuildProxies[name] = new Ctor(); }); var SymbolClz = extendShape({ type: 'symbol', shape: { symbolType: '', x: 0, y: 0, width: 0, height: 0 }, calculateTextPosition: function (out, style, rect) { var res = calculateTextPosition(out, style, rect); var shape = this.shape; if (shape && shape.symbolType === 'pin' && style.textPosition === 'inside') { res.y = rect.y + rect.height * 0.4; } return res; }, buildPath: function (ctx, shape, inBundle) { var symbolType = shape.symbolType; if (symbolType !== 'none') { var proxySymbol = symbolBuildProxies[symbolType]; if (!proxySymbol) { // Default rect symbolType = 'rect'; proxySymbol = symbolBuildProxies[symbolType]; } symbolShapeMakers[symbolType]( shape.x, shape.y, shape.width, shape.height, proxySymbol.shape ); proxySymbol.buildPath(ctx, proxySymbol.shape, inBundle); } } }); // Provide setColor helper method to avoid determine if set the fill or stroke outside function symbolPathSetColor(color, innerColor) { if (this.type !== 'image') { var symbolStyle = this.style; var symbolShape = this.shape; if (symbolShape && symbolShape.symbolType === 'line') { symbolStyle.stroke = color; } else if (this.__isEmptyBrush) { symbolStyle.stroke = color; symbolStyle.fill = innerColor || '#fff'; } else { // FIXME 判断图形默认是填充还是描边,使用 onlyStroke ? symbolStyle.fill && (symbolStyle.fill = color); symbolStyle.stroke && (symbolStyle.stroke = color); } this.dirty(false); } } /** * Create a symbol element with given symbol configuration: shape, x, y, width, height, color * @param {string} symbolType * @param {number} x * @param {number} y * @param {number} w * @param {number} h * @param {string} color * @param {boolean} [keepAspect=false] whether to keep the ratio of w/h, * for path and image only. */ function createSymbol(symbolType, x, y, w, h, color, keepAspect) { // TODO Support image object, DynamicImage. var isEmpty = symbolType.indexOf('empty') === 0; if (isEmpty) { symbolType = symbolType.substr(5, 1).toLowerCase() + symbolType.substr(6); } var symbolPath; if (symbolType.indexOf('image://') === 0) { symbolPath = makeImage( symbolType.slice(8), new BoundingRect(x, y, w, h), keepAspect ? 'center' : 'cover' ); } else if (symbolType.indexOf('path://') === 0) { symbolPath = makePath( symbolType.slice(7), {}, new BoundingRect(x, y, w, h), keepAspect ? 'center' : 'cover' ); } else { symbolPath = new SymbolClz({ shape: { symbolType: symbolType, x: x, y: y, width: w, height: h } }); } symbolPath.__isEmptyBrush = isEmpty; symbolPath.setColor = symbolPathSetColor; symbolPath.setColor(color); return symbolPath; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import createGraphFromNodeEdge from './chart/helper/createGraphFromNodeEdge'; /** * Create a muti dimension List structure from seriesModel. * @param {module:echarts/model/Model} seriesModel * @return {module:echarts/data/List} list */ function createList(seriesModel) { return createListFromArray(seriesModel.getSource(), seriesModel); } var dataStack$1 = { isDimensionStacked: isDimensionStacked, enableDataStack: enableDataStack, getStackedDimension: getStackedDimension }; /** * Create scale * @param {Array.} dataExtent * @param {Object|module:echarts/Model} option */ function createScale(dataExtent, option) { var axisModel = option; if (!Model.isInstance(option)) { axisModel = new Model(option); mixin(axisModel, axisModelCommonMixin); } var scale = createScaleByModel(axisModel); scale.setExtent(dataExtent[0], dataExtent[1]); niceScaleExtent(scale, axisModel); return scale; } /** * Mixin common methods to axis model, * * Inlcude methods * `getFormattedLabels() => Array.` * `getCategories() => Array.` * `getMin(origin: boolean) => number` * `getMax(origin: boolean) => number` * `getNeedCrossZero() => boolean` * `setRange(start: number, end: number)` * `resetRange()` */ function mixinAxisModelCommonMethods(Model$$1) { mixin(Model$$1, axisModelCommonMixin); } var helper = (Object.freeze || Object)({ createList: createList, getLayoutRect: getLayoutRect, dataStack: dataStack$1, createScale: createScale, mixinAxisModelCommonMethods: mixinAxisModelCommonMethods, completeDimensions: completeDimensions, createDimensions: createDimensions, createSymbol: createSymbol }); var EPSILON$3 = 1e-8; function isAroundEqual$1(a, b) { return Math.abs(a - b) < EPSILON$3; } function contain$1(points, x, y) { var w = 0; var p = points[0]; if (!p) { return false; } for (var i = 1; i < points.length; i++) { var p2 = points[i]; w += windingLine(p[0], p[1], p2[0], p2[1], x, y); p = p2; } // Close polygon var p0 = points[0]; if (!isAroundEqual$1(p[0], p0[0]) || !isAroundEqual$1(p[1], p0[1])) { w += windingLine(p[0], p[1], p0[0], p0[1], x, y); } return w !== 0; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/coord/geo/Region */ /** * @param {string|Region} name * @param {Array} geometries * @param {Array.} cp */ function Region(name, geometries, cp) { /** * @type {string} * @readOnly */ this.name = name; /** * @type {Array.} * @readOnly */ this.geometries = geometries; if (!cp) { var rect = this.getBoundingRect(); cp = [ rect.x + rect.width / 2, rect.y + rect.height / 2 ]; } else { cp = [cp[0], cp[1]]; } /** * @type {Array.} */ this.center = cp; } Region.prototype = { constructor: Region, properties: null, /** * @return {module:zrender/core/BoundingRect} */ getBoundingRect: function () { var rect = this._rect; if (rect) { return rect; } var MAX_NUMBER = Number.MAX_VALUE; var min$$1 = [MAX_NUMBER, MAX_NUMBER]; var max$$1 = [-MAX_NUMBER, -MAX_NUMBER]; var min2 = []; var max2 = []; var geometries = this.geometries; for (var i = 0; i < geometries.length; i++) { // Only support polygon if (geometries[i].type !== 'polygon') { continue; } // Doesn't consider hole var exterior = geometries[i].exterior; fromPoints(exterior, min2, max2); min(min$$1, min$$1, min2); max(max$$1, max$$1, max2); } // No data if (i === 0) { min$$1[0] = min$$1[1] = max$$1[0] = max$$1[1] = 0; } return (this._rect = new BoundingRect( min$$1[0], min$$1[1], max$$1[0] - min$$1[0], max$$1[1] - min$$1[1] )); }, /** * @param {} coord * @return {boolean} */ contain: function (coord) { var rect = this.getBoundingRect(); var geometries = this.geometries; if (!rect.contain(coord[0], coord[1])) { return false; } loopGeo: for (var i = 0, len$$1 = geometries.length; i < len$$1; i++) { // Only support polygon. if (geometries[i].type !== 'polygon') { continue; } var exterior = geometries[i].exterior; var interiors = geometries[i].interiors; if (contain$1(exterior, coord[0], coord[1])) { // Not in the region if point is in the hole. for (var k = 0; k < (interiors ? interiors.length : 0); k++) { if (contain$1(interiors[k])) { continue loopGeo; } } return true; } } return false; }, transformTo: function (x, y, width, height) { var rect = this.getBoundingRect(); var aspect = rect.width / rect.height; if (!width) { width = aspect * height; } else if (!height) { height = width / aspect; } var target = new BoundingRect(x, y, width, height); var transform = rect.calculateTransform(target); var geometries = this.geometries; for (var i = 0; i < geometries.length; i++) { // Only support polygon. if (geometries[i].type !== 'polygon') { continue; } var exterior = geometries[i].exterior; var interiors = geometries[i].interiors; for (var p = 0; p < exterior.length; p++) { applyTransform(exterior[p], exterior[p], transform); } for (var h = 0; h < (interiors ? interiors.length : 0); h++) { for (var p = 0; p < interiors[h].length; p++) { applyTransform(interiors[h][p], interiors[h][p], transform); } } } rect = this._rect; rect.copy(target); // Update center this.center = [ rect.x + rect.width / 2, rect.y + rect.height / 2 ]; }, cloneShallow: function (name) { name == null && (name = this.name); var newRegion = new Region(name, this.geometries, this.center); newRegion._rect = this._rect; newRegion.transformTo = null; // Simply avoid to be called. return newRegion; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Parse and decode geo json * @module echarts/coord/geo/parseGeoJson */ function decode(json) { if (!json.UTF8Encoding) { return json; } var encodeScale = json.UTF8Scale; if (encodeScale == null) { encodeScale = 1024; } var features = json.features; for (var f = 0; f < features.length; f++) { var feature = features[f]; var geometry = feature.geometry; var coordinates = geometry.coordinates; var encodeOffsets = geometry.encodeOffsets; for (var c = 0; c < coordinates.length; c++) { var coordinate = coordinates[c]; if (geometry.type === 'Polygon') { coordinates[c] = decodePolygon( coordinate, encodeOffsets[c], encodeScale ); } else if (geometry.type === 'MultiPolygon') { for (var c2 = 0; c2 < coordinate.length; c2++) { var polygon = coordinate[c2]; coordinate[c2] = decodePolygon( polygon, encodeOffsets[c][c2], encodeScale ); } } } } // Has been decoded json.UTF8Encoding = false; return json; } function decodePolygon(coordinate, encodeOffsets, encodeScale) { var result = []; var prevX = encodeOffsets[0]; var prevY = encodeOffsets[1]; for (var i = 0; i < coordinate.length; i += 2) { var x = coordinate.charCodeAt(i) - 64; var y = coordinate.charCodeAt(i + 1) - 64; // ZigZag decoding x = (x >> 1) ^ (-(x & 1)); y = (y >> 1) ^ (-(y & 1)); // Delta deocding x += prevX; y += prevY; prevX = x; prevY = y; // Dequantize result.push([x / encodeScale, y / encodeScale]); } return result; } /** * @alias module:echarts/coord/geo/parseGeoJson * @param {Object} geoJson * @param {string} nameProperty * @return {module:zrender/container/Group} */ var parseGeoJSON = function (geoJson, nameProperty) { decode(geoJson); return map(filter(geoJson.features, function (featureObj) { // Output of mapshaper may have geometry null return featureObj.geometry && featureObj.properties && featureObj.geometry.coordinates.length > 0; }), function (featureObj) { var properties = featureObj.properties; var geo = featureObj.geometry; var coordinates = geo.coordinates; var geometries = []; if (geo.type === 'Polygon') { geometries.push({ type: 'polygon', // According to the GeoJSON specification. // First must be exterior, and the rest are all interior(holes). exterior: coordinates[0], interiors: coordinates.slice(1) }); } if (geo.type === 'MultiPolygon') { each$1(coordinates, function (item) { if (item[0]) { geometries.push({ type: 'polygon', exterior: item[0], interiors: item.slice(1) }); } }); } var region = new Region( properties[nameProperty || 'name'], geometries, properties.cp ); region.properties = properties; return region; }); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$6 = makeInner(); /** * @param {module:echats/coord/Axis} axis * @return {Object} { * labels: [{ * formattedLabel: string, * rawLabel: string, * tickValue: number * }, ...], * labelCategoryInterval: number * } */ function createAxisLabels(axis) { // Only ordinal scale support tick interval return axis.type === 'category' ? makeCategoryLabels(axis) : makeRealNumberLabels(axis); } /** * @param {module:echats/coord/Axis} axis * @param {module:echarts/model/Model} tickModel For example, can be axisTick, splitLine, splitArea. * @return {Object} { * ticks: Array. * tickCategoryInterval: number * } */ function createAxisTicks(axis, tickModel) { // Only ordinal scale support tick interval return axis.type === 'category' ? makeCategoryTicks(axis, tickModel) : {ticks: axis.scale.getTicks()}; } function makeCategoryLabels(axis) { var labelModel = axis.getLabelModel(); var result = makeCategoryLabelsActually(axis, labelModel); return (!labelModel.get('show') || axis.scale.isBlank()) ? {labels: [], labelCategoryInterval: result.labelCategoryInterval} : result; } function makeCategoryLabelsActually(axis, labelModel) { var labelsCache = getListCache(axis, 'labels'); var optionLabelInterval = getOptionCategoryInterval(labelModel); var result = listCacheGet(labelsCache, optionLabelInterval); if (result) { return result; } var labels; var numericLabelInterval; if (isFunction$1(optionLabelInterval)) { labels = makeLabelsByCustomizedCategoryInterval(axis, optionLabelInterval); } else { numericLabelInterval = optionLabelInterval === 'auto' ? makeAutoCategoryInterval(axis) : optionLabelInterval; labels = makeLabelsByNumericCategoryInterval(axis, numericLabelInterval); } // Cache to avoid calling interval function repeatly. return listCacheSet(labelsCache, optionLabelInterval, { labels: labels, labelCategoryInterval: numericLabelInterval }); } function makeCategoryTicks(axis, tickModel) { var ticksCache = getListCache(axis, 'ticks'); var optionTickInterval = getOptionCategoryInterval(tickModel); var result = listCacheGet(ticksCache, optionTickInterval); if (result) { return result; } var ticks; var tickCategoryInterval; // Optimize for the case that large category data and no label displayed, // we should not return all ticks. if (!tickModel.get('show') || axis.scale.isBlank()) { ticks = []; } if (isFunction$1(optionTickInterval)) { ticks = makeLabelsByCustomizedCategoryInterval(axis, optionTickInterval, true); } // Always use label interval by default despite label show. Consider this // scenario, Use multiple grid with the xAxis sync, and only one xAxis shows // labels. `splitLine` and `axisTick` should be consistent in this case. else if (optionTickInterval === 'auto') { var labelsResult = makeCategoryLabelsActually(axis, axis.getLabelModel()); tickCategoryInterval = labelsResult.labelCategoryInterval; ticks = map(labelsResult.labels, function (labelItem) { return labelItem.tickValue; }); } else { tickCategoryInterval = optionTickInterval; ticks = makeLabelsByNumericCategoryInterval(axis, tickCategoryInterval, true); } // Cache to avoid calling interval function repeatly. return listCacheSet(ticksCache, optionTickInterval, { ticks: ticks, tickCategoryInterval: tickCategoryInterval }); } function makeRealNumberLabels(axis) { var ticks = axis.scale.getTicks(); var labelFormatter = makeLabelFormatter(axis); return { labels: map(ticks, function (tickValue, idx) { return { formattedLabel: labelFormatter(tickValue, idx), rawLabel: axis.scale.getLabel(tickValue), tickValue: tickValue }; }) }; } // Large category data calculation is performence sensitive, and ticks and label // probably be fetched by multiple times. So we cache the result. // axis is created each time during a ec process, so we do not need to clear cache. function getListCache(axis, prop) { // Because key can be funciton, and cache size always be small, we use array cache. return inner$6(axis)[prop] || (inner$6(axis)[prop] = []); } function listCacheGet(cache, key) { for (var i = 0; i < cache.length; i++) { if (cache[i].key === key) { return cache[i].value; } } } function listCacheSet(cache, key, value) { cache.push({key: key, value: value}); return value; } function makeAutoCategoryInterval(axis) { var result = inner$6(axis).autoInterval; return result != null ? result : (inner$6(axis).autoInterval = axis.calculateCategoryInterval()); } /** * Calculate interval for category axis ticks and labels. * To get precise result, at least one of `getRotate` and `isHorizontal` * should be implemented in axis. */ function calculateCategoryInterval(axis) { var params = fetchAutoCategoryIntervalCalculationParams(axis); var labelFormatter = makeLabelFormatter(axis); var rotation = (params.axisRotate - params.labelRotate) / 180 * Math.PI; var ordinalScale = axis.scale; var ordinalExtent = ordinalScale.getExtent(); // Providing this method is for optimization: // avoid generating a long array by `getTicks` // in large category data case. var tickCount = ordinalScale.count(); if (ordinalExtent[1] - ordinalExtent[0] < 1) { return 0; } var step = 1; // Simple optimization. Empirical value: tick count should less than 40. if (tickCount > 40) { step = Math.max(1, Math.floor(tickCount / 40)); } var tickValue = ordinalExtent[0]; var unitSpan = axis.dataToCoord(tickValue + 1) - axis.dataToCoord(tickValue); var unitW = Math.abs(unitSpan * Math.cos(rotation)); var unitH = Math.abs(unitSpan * Math.sin(rotation)); var maxW = 0; var maxH = 0; // Caution: Performance sensitive for large category data. // Consider dataZoom, we should make appropriate step to avoid O(n) loop. for (; tickValue <= ordinalExtent[1]; tickValue += step) { var width = 0; var height = 0; // Not precise, do not consider align and vertical align // and each distance from axis line yet. var rect = getBoundingRect( labelFormatter(tickValue), params.font, 'center', 'top' ); // Magic number width = rect.width * 1.3; height = rect.height * 1.3; // Min size, void long loop. maxW = Math.max(maxW, width, 7); maxH = Math.max(maxH, height, 7); } var dw = maxW / unitW; var dh = maxH / unitH; // 0/0 is NaN, 1/0 is Infinity. isNaN(dw) && (dw = Infinity); isNaN(dh) && (dh = Infinity); var interval = Math.max(0, Math.floor(Math.min(dw, dh))); var cache = inner$6(axis.model); var axisExtent = axis.getExtent(); var lastAutoInterval = cache.lastAutoInterval; var lastTickCount = cache.lastTickCount; // Use cache to keep interval stable while moving zoom window, // otherwise the calculated interval might jitter when the zoom // window size is close to the interval-changing size. // For example, if all of the axis labels are `a, b, c, d, e, f, g`. // The jitter will cause that sometimes the displayed labels are // `a, d, g` (interval: 2) sometimes `a, c, e`(interval: 1). if (lastAutoInterval != null && lastTickCount != null && Math.abs(lastAutoInterval - interval) <= 1 && Math.abs(lastTickCount - tickCount) <= 1 // Always choose the bigger one, otherwise the critical // point is not the same when zooming in or zooming out. && lastAutoInterval > interval // If the axis change is caused by chart resize, the cache should not // be used. Otherwise some hiden labels might not be shown again. && cache.axisExtend0 === axisExtent[0] && cache.axisExtend1 === axisExtent[1] ) { interval = lastAutoInterval; } // Only update cache if cache not used, otherwise the // changing of interval is too insensitive. else { cache.lastTickCount = tickCount; cache.lastAutoInterval = interval; cache.axisExtend0 = axisExtent[0]; cache.axisExtend1 = axisExtent[1]; } return interval; } function fetchAutoCategoryIntervalCalculationParams(axis) { var labelModel = axis.getLabelModel(); return { axisRotate: axis.getRotate ? axis.getRotate() : (axis.isHorizontal && !axis.isHorizontal()) ? 90 : 0, labelRotate: labelModel.get('rotate') || 0, font: labelModel.getFont() }; } function makeLabelsByNumericCategoryInterval(axis, categoryInterval, onlyTick) { var labelFormatter = makeLabelFormatter(axis); var ordinalScale = axis.scale; var ordinalExtent = ordinalScale.getExtent(); var labelModel = axis.getLabelModel(); var result = []; // TODO: axisType: ordinalTime, pick the tick from each month/day/year/... var step = Math.max((categoryInterval || 0) + 1, 1); var startTick = ordinalExtent[0]; var tickCount = ordinalScale.count(); // Calculate start tick based on zero if possible to keep label consistent // while zooming and moving while interval > 0. Otherwise the selection // of displayable ticks and symbols probably keep changing. // 3 is empirical value. if (startTick !== 0 && step > 1 && tickCount / step > 2) { startTick = Math.round(Math.ceil(startTick / step) * step); } // (1) Only add min max label here but leave overlap checking // to render stage, which also ensure the returned list // suitable for splitLine and splitArea rendering. // (2) Scales except category always contain min max label so // do not need to perform this process. var showAllLabel = shouldShowAllLabels(axis); var includeMinLabel = labelModel.get('showMinLabel') || showAllLabel; var includeMaxLabel = labelModel.get('showMaxLabel') || showAllLabel; if (includeMinLabel && startTick !== ordinalExtent[0]) { addItem(ordinalExtent[0]); } // Optimize: avoid generating large array by `ordinalScale.getTicks()`. var tickValue = startTick; for (; tickValue <= ordinalExtent[1]; tickValue += step) { addItem(tickValue); } if (includeMaxLabel && tickValue - step !== ordinalExtent[1]) { addItem(ordinalExtent[1]); } function addItem(tVal) { result.push(onlyTick ? tVal : { formattedLabel: labelFormatter(tVal), rawLabel: ordinalScale.getLabel(tVal), tickValue: tVal } ); } return result; } // When interval is function, the result `false` means ignore the tick. // It is time consuming for large category data. function makeLabelsByCustomizedCategoryInterval(axis, categoryInterval, onlyTick) { var ordinalScale = axis.scale; var labelFormatter = makeLabelFormatter(axis); var result = []; each$1(ordinalScale.getTicks(), function (tickValue) { var rawLabel = ordinalScale.getLabel(tickValue); if (categoryInterval(tickValue, rawLabel)) { result.push(onlyTick ? tickValue : { formattedLabel: labelFormatter(tickValue), rawLabel: rawLabel, tickValue: tickValue } ); } }); return result; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var NORMALIZED_EXTENT = [0, 1]; /** * Base class of Axis. * @constructor */ var Axis = function (dim, scale, extent) { /** * Axis dimension. Such as 'x', 'y', 'z', 'angle', 'radius'. * @type {string} */ this.dim = dim; /** * Axis scale * @type {module:echarts/coord/scale/*} */ this.scale = scale; /** * @type {Array.} * @private */ this._extent = extent || [0, 0]; /** * @type {boolean} */ this.inverse = false; /** * Usually true when axis has a ordinal scale * @type {boolean} */ this.onBand = false; }; Axis.prototype = { constructor: Axis, /** * If axis extent contain given coord * @param {number} coord * @return {boolean} */ contain: function (coord) { var extent = this._extent; var min = Math.min(extent[0], extent[1]); var max = Math.max(extent[0], extent[1]); return coord >= min && coord <= max; }, /** * If axis extent contain given data * @param {number} data * @return {boolean} */ containData: function (data) { return this.scale.contain(data); }, /** * Get coord extent. * @return {Array.} */ getExtent: function () { return this._extent.slice(); }, /** * Get precision used for formatting * @param {Array.} [dataExtent] * @return {number} */ getPixelPrecision: function (dataExtent) { return getPixelPrecision( dataExtent || this.scale.getExtent(), this._extent ); }, /** * Set coord extent * @param {number} start * @param {number} end */ setExtent: function (start, end) { var extent = this._extent; extent[0] = start; extent[1] = end; }, /** * Convert data to coord. Data is the rank if it has an ordinal scale * @param {number} data * @param {boolean} clamp * @return {number} */ dataToCoord: function (data, clamp) { var extent = this._extent; var scale = this.scale; data = scale.normalize(data); if (this.onBand && scale.type === 'ordinal') { extent = extent.slice(); fixExtentWithBands(extent, scale.count()); } return linearMap(data, NORMALIZED_EXTENT, extent, clamp); }, /** * Convert coord to data. Data is the rank if it has an ordinal scale * @param {number} coord * @param {boolean} clamp * @return {number} */ coordToData: function (coord, clamp) { var extent = this._extent; var scale = this.scale; if (this.onBand && scale.type === 'ordinal') { extent = extent.slice(); fixExtentWithBands(extent, scale.count()); } var t = linearMap(coord, extent, NORMALIZED_EXTENT, clamp); return this.scale.scale(t); }, /** * Convert pixel point to data in axis * @param {Array.} point * @param {boolean} clamp * @return {number} data */ pointToData: function (point, clamp) { // Should be implemented in derived class if necessary. }, /** * Different from `zrUtil.map(axis.getTicks(), axis.dataToCoord, axis)`, * `axis.getTicksCoords` considers `onBand`, which is used by * `boundaryGap:true` of category axis and splitLine and splitArea. * @param {Object} [opt] * @param {Model} [opt.tickModel=axis.model.getModel('axisTick')] * @param {boolean} [opt.clamp] If `true`, the first and the last * tick must be at the axis end points. Otherwise, clip ticks * that outside the axis extent. * @return {Array.} [{ * coord: ..., * tickValue: ... * }, ...] */ getTicksCoords: function (opt) { opt = opt || {}; var tickModel = opt.tickModel || this.getTickModel(); var result = createAxisTicks(this, tickModel); var ticks = result.ticks; var ticksCoords = map(ticks, function (tickValue) { return { coord: this.dataToCoord(tickValue), tickValue: tickValue }; }, this); var alignWithLabel = tickModel.get('alignWithLabel'); fixOnBandTicksCoords( this, ticksCoords, alignWithLabel, opt.clamp ); return ticksCoords; }, /** * @return {Array.>} [{ coord: ..., tickValue: ...}] */ getMinorTicksCoords: function () { if (this.scale.type === 'ordinal') { // Category axis doesn't support minor ticks return []; } var minorTickModel = this.model.getModel('minorTick'); var splitNumber = minorTickModel.get('splitNumber'); // Protection. if (!(splitNumber > 0 && splitNumber < 100)) { splitNumber = 5; } var minorTicks = this.scale.getMinorTicks(splitNumber); var minorTicksCoords = map(minorTicks, function (minorTicksGroup) { return map(minorTicksGroup, function (minorTick) { return { coord: this.dataToCoord(minorTick), tickValue: minorTick }; }, this); }, this); return minorTicksCoords; }, /** * @return {Array.} [{ * formattedLabel: string, * rawLabel: axis.scale.getLabel(tickValue) * tickValue: number * }, ...] */ getViewLabels: function () { return createAxisLabels(this).labels; }, /** * @return {module:echarts/coord/model/Model} */ getLabelModel: function () { return this.model.getModel('axisLabel'); }, /** * Notice here we only get the default tick model. For splitLine * or splitArea, we should pass the splitLineModel or splitAreaModel * manually when calling `getTicksCoords`. * In GL, this method may be overrided to: * `axisModel.getModel('axisTick', grid3DModel.getModel('axisTick'));` * @return {module:echarts/coord/model/Model} */ getTickModel: function () { return this.model.getModel('axisTick'); }, /** * Get width of band * @return {number} */ getBandWidth: function () { var axisExtent = this._extent; var dataExtent = this.scale.getExtent(); var len = dataExtent[1] - dataExtent[0] + (this.onBand ? 1 : 0); // Fix #2728, avoid NaN when only one data. len === 0 && (len = 1); var size = Math.abs(axisExtent[1] - axisExtent[0]); return Math.abs(size) / len; }, /** * @abstract * @return {boolean} Is horizontal */ isHorizontal: null, /** * @abstract * @return {number} Get axis rotate, by degree. */ getRotate: null, /** * Only be called in category axis. * Can be overrided, consider other axes like in 3D. * @return {number} Auto interval for cateogry axis tick and label */ calculateCategoryInterval: function () { return calculateCategoryInterval(this); } }; function fixExtentWithBands(extent, nTick) { var size = extent[1] - extent[0]; var len = nTick; var margin = size / len / 2; extent[0] += margin; extent[1] -= margin; } // If axis has labels [1, 2, 3, 4]. Bands on the axis are // |---1---|---2---|---3---|---4---|. // So the displayed ticks and splitLine/splitArea should between // each data item, otherwise cause misleading (e.g., split tow bars // of a single data item when there are two bar series). // Also consider if tickCategoryInterval > 0 and onBand, ticks and // splitLine/spliteArea should layout appropriately corresponding // to displayed labels. (So we should not use `getBandWidth` in this // case). function fixOnBandTicksCoords(axis, ticksCoords, alignWithLabel, clamp) { var ticksLen = ticksCoords.length; if (!axis.onBand || alignWithLabel || !ticksLen) { return; } var axisExtent = axis.getExtent(); var last; var diffSize; if (ticksLen === 1) { ticksCoords[0].coord = axisExtent[0]; last = ticksCoords[1] = {coord: axisExtent[0]}; } else { var crossLen = ticksCoords[ticksLen - 1].tickValue - ticksCoords[0].tickValue; var shift = (ticksCoords[ticksLen - 1].coord - ticksCoords[0].coord) / crossLen; each$1(ticksCoords, function (ticksItem) { ticksItem.coord -= shift / 2; }); var dataExtent = axis.scale.getExtent(); diffSize = 1 + dataExtent[1] - ticksCoords[ticksLen - 1].tickValue; last = {coord: ticksCoords[ticksLen - 1].coord + shift * diffSize}; ticksCoords.push(last); } var inverse = axisExtent[0] > axisExtent[1]; // Handling clamp. if (littleThan(ticksCoords[0].coord, axisExtent[0])) { clamp ? (ticksCoords[0].coord = axisExtent[0]) : ticksCoords.shift(); } if (clamp && littleThan(axisExtent[0], ticksCoords[0].coord)) { ticksCoords.unshift({coord: axisExtent[0]}); } if (littleThan(axisExtent[1], last.coord)) { clamp ? (last.coord = axisExtent[1]) : ticksCoords.pop(); } if (clamp && littleThan(last.coord, axisExtent[1])) { ticksCoords.push({coord: axisExtent[1]}); } function littleThan(a, b) { // Avoid rounding error cause calculated tick coord different with extent. // It may cause an extra unecessary tick added. a = round$1(a); b = round$1(b); return inverse ? a > b : a < b; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Do not mount those modules on 'src/echarts' for better tree shaking. */ var parseGeoJson = parseGeoJSON; var ecUtil = {}; each$1( [ 'map', 'each', 'filter', 'indexOf', 'inherits', 'reduce', 'filter', 'bind', 'curry', 'isArray', 'isString', 'isObject', 'isFunction', 'extend', 'defaults', 'clone', 'merge' ], function (name) { ecUtil[name] = zrUtil[name]; } ); var graphic$1 = {}; each$1( [ 'extendShape', 'extendPath', 'makePath', 'makeImage', 'mergePath', 'resizePath', 'createIcon', 'setHoverStyle', 'setLabelStyle', 'setTextStyle', 'setText', 'getFont', 'updateProps', 'initProps', 'getTransform', 'clipPointsByRect', 'clipRectByRect', 'registerShape', 'getShapeClass', 'Group', 'Image', 'Text', 'Circle', 'Sector', 'Ring', 'Polygon', 'Polyline', 'Rect', 'Line', 'BezierCurve', 'Arc', 'IncrementalDisplayable', 'CompoundPath', 'LinearGradient', 'RadialGradient', 'BoundingRect' ], function (name) { graphic$1[name] = graphic[name]; } ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ SeriesModel.extend({ type: 'series.line', dependencies: ['grid', 'polar'], getInitialData: function (option, ecModel) { if (__DEV__) { var coordSys = option.coordinateSystem; if (coordSys !== 'polar' && coordSys !== 'cartesian2d') { throw new Error('Line not support coordinateSystem besides cartesian and polar'); } } return createListFromArray(this.getSource(), this, {useEncodeDefaulter: true}); }, defaultOption: { zlevel: 0, z: 2, coordinateSystem: 'cartesian2d', legendHoverLink: true, hoverAnimation: true, // stack: null // xAxisIndex: 0, // yAxisIndex: 0, // polarIndex: 0, // If clip the overflow value clip: true, // cursor: null, label: { position: 'top' }, // itemStyle: { // }, lineStyle: { width: 2, type: 'solid' }, // areaStyle: { // origin of areaStyle. Valid values: // `'auto'/null/undefined`: from axisLine to data // `'start'`: from min to data // `'end'`: from data to max // origin: 'auto' // }, // false, 'start', 'end', 'middle' step: false, // Disabled if step is true smooth: false, smoothMonotone: null, symbol: 'emptyCircle', symbolSize: 4, symbolRotate: null, showSymbol: true, // `false`: follow the label interval strategy. // `true`: show all symbols. // `'auto'`: If possible, show all symbols, otherwise // follow the label interval strategy. showAllSymbol: 'auto', // Whether to connect break point. connectNulls: false, // Sampling for large data. Can be: 'average', 'max', 'min', 'sum'. sampling: 'none', animationEasing: 'linear', // Disable progressive progressive: 0, hoverLayerThreshold: Infinity } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {module:echarts/data/List} data * @param {number} dataIndex * @return {string} label string. Not null/undefined */ function getDefaultLabel(data, dataIndex) { var labelDims = data.mapDimension('defaultedLabel', true); var len = labelDims.length; // Simple optimization (in lots of cases, label dims length is 1) if (len === 1) { return retrieveRawValue(data, dataIndex, labelDims[0]); } else if (len) { var vals = []; for (var i = 0; i < labelDims.length; i++) { var val = retrieveRawValue(data, dataIndex, labelDims[i]); vals.push(val); } return vals.join(' '); } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/chart/helper/Symbol */ /** * @constructor * @alias {module:echarts/chart/helper/Symbol} * @param {module:echarts/data/List} data * @param {number} idx * @extends {module:zrender/graphic/Group} */ function SymbolClz$1(data, idx, seriesScope) { Group.call(this); this.updateData(data, idx, seriesScope); } var symbolProto = SymbolClz$1.prototype; /** * @public * @static * @param {module:echarts/data/List} data * @param {number} dataIndex * @return {Array.} [width, height] */ var getSymbolSize = SymbolClz$1.getSymbolSize = function (data, idx) { var symbolSize = data.getItemVisual(idx, 'symbolSize'); return symbolSize instanceof Array ? symbolSize.slice() : [+symbolSize, +symbolSize]; }; function getScale(symbolSize) { return [symbolSize[0] / 2, symbolSize[1] / 2]; } function driftSymbol(dx, dy) { this.parent.drift(dx, dy); } symbolProto._createSymbol = function ( symbolType, data, idx, symbolSize, keepAspect ) { // Remove paths created before this.removeAll(); var color = data.getItemVisual(idx, 'color'); // var symbolPath = createSymbol( // symbolType, -0.5, -0.5, 1, 1, color // ); // If width/height are set too small (e.g., set to 1) on ios10 // and macOS Sierra, a circle stroke become a rect, no matter what // the scale is set. So we set width/height as 2. See #4150. var symbolPath = createSymbol( symbolType, -1, -1, 2, 2, color, keepAspect ); symbolPath.attr({ z2: 100, culling: true, scale: getScale(symbolSize) }); // Rewrite drift method symbolPath.drift = driftSymbol; this._symbolType = symbolType; this.add(symbolPath); }; /** * Stop animation * @param {boolean} toLastFrame */ symbolProto.stopSymbolAnimation = function (toLastFrame) { this.childAt(0).stopAnimation(toLastFrame); }; /** * FIXME: * Caution: This method breaks the encapsulation of this module, * but it indeed brings convenience. So do not use the method * unless you detailedly know all the implements of `Symbol`, * especially animation. * * Get symbol path element. */ symbolProto.getSymbolPath = function () { return this.childAt(0); }; /** * Get scale(aka, current symbol size). * Including the change caused by animation */ symbolProto.getScale = function () { return this.childAt(0).scale; }; /** * Highlight symbol */ symbolProto.highlight = function () { this.childAt(0).trigger('emphasis'); }; /** * Downplay symbol */ symbolProto.downplay = function () { this.childAt(0).trigger('normal'); }; /** * @param {number} zlevel * @param {number} z */ symbolProto.setZ = function (zlevel, z) { var symbolPath = this.childAt(0); symbolPath.zlevel = zlevel; symbolPath.z = z; }; symbolProto.setDraggable = function (draggable) { var symbolPath = this.childAt(0); symbolPath.draggable = draggable; symbolPath.cursor = draggable ? 'move' : symbolPath.cursor; }; /** * Update symbol properties * @param {module:echarts/data/List} data * @param {number} idx * @param {Object} [seriesScope] * @param {Object} [seriesScope.itemStyle] * @param {Object} [seriesScope.hoverItemStyle] * @param {Object} [seriesScope.symbolRotate] * @param {Object} [seriesScope.symbolOffset] * @param {module:echarts/model/Model} [seriesScope.labelModel] * @param {module:echarts/model/Model} [seriesScope.hoverLabelModel] * @param {boolean} [seriesScope.hoverAnimation] * @param {Object} [seriesScope.cursorStyle] * @param {module:echarts/model/Model} [seriesScope.itemModel] * @param {string} [seriesScope.symbolInnerColor] * @param {Object} [seriesScope.fadeIn=false] */ symbolProto.updateData = function (data, idx, seriesScope) { this.silent = false; var symbolType = data.getItemVisual(idx, 'symbol') || 'circle'; var seriesModel = data.hostModel; var symbolSize = getSymbolSize(data, idx); var isInit = symbolType !== this._symbolType; if (isInit) { var keepAspect = data.getItemVisual(idx, 'symbolKeepAspect'); this._createSymbol(symbolType, data, idx, symbolSize, keepAspect); } else { var symbolPath = this.childAt(0); symbolPath.silent = false; updateProps(symbolPath, { scale: getScale(symbolSize) }, seriesModel, idx); } this._updateCommon(data, idx, symbolSize, seriesScope); if (isInit) { var symbolPath = this.childAt(0); var fadeIn = seriesScope && seriesScope.fadeIn; var target = {scale: symbolPath.scale.slice()}; fadeIn && (target.style = {opacity: symbolPath.style.opacity}); symbolPath.scale = [0, 0]; fadeIn && (symbolPath.style.opacity = 0); initProps(symbolPath, target, seriesModel, idx); } this._seriesModel = seriesModel; }; // Update common properties var normalStyleAccessPath = ['itemStyle']; var emphasisStyleAccessPath = ['emphasis', 'itemStyle']; var normalLabelAccessPath = ['label']; var emphasisLabelAccessPath = ['emphasis', 'label']; /** * @param {module:echarts/data/List} data * @param {number} idx * @param {Array.} symbolSize * @param {Object} [seriesScope] */ symbolProto._updateCommon = function (data, idx, symbolSize, seriesScope) { var symbolPath = this.childAt(0); var seriesModel = data.hostModel; var color = data.getItemVisual(idx, 'color'); // Reset style if (symbolPath.type !== 'image') { symbolPath.useStyle({ strokeNoScale: true }); } else { symbolPath.setStyle({ opacity: 1, shadowBlur: null, shadowOffsetX: null, shadowOffsetY: null, shadowColor: null }); } var itemStyle = seriesScope && seriesScope.itemStyle; var hoverItemStyle = seriesScope && seriesScope.hoverItemStyle; var symbolOffset = seriesScope && seriesScope.symbolOffset; var labelModel = seriesScope && seriesScope.labelModel; var hoverLabelModel = seriesScope && seriesScope.hoverLabelModel; var hoverAnimation = seriesScope && seriesScope.hoverAnimation; var cursorStyle = seriesScope && seriesScope.cursorStyle; if (!seriesScope || data.hasItemOption) { var itemModel = (seriesScope && seriesScope.itemModel) ? seriesScope.itemModel : data.getItemModel(idx); // Color must be excluded. // Because symbol provide setColor individually to set fill and stroke itemStyle = itemModel.getModel(normalStyleAccessPath).getItemStyle(['color']); hoverItemStyle = itemModel.getModel(emphasisStyleAccessPath).getItemStyle(); symbolOffset = itemModel.getShallow('symbolOffset'); labelModel = itemModel.getModel(normalLabelAccessPath); hoverLabelModel = itemModel.getModel(emphasisLabelAccessPath); hoverAnimation = itemModel.getShallow('hoverAnimation'); cursorStyle = itemModel.getShallow('cursor'); } else { hoverItemStyle = extend({}, hoverItemStyle); } var elStyle = symbolPath.style; var symbolRotate = data.getItemVisual(idx, 'symbolRotate'); symbolPath.attr('rotation', (symbolRotate || 0) * Math.PI / 180 || 0); if (symbolOffset) { symbolPath.attr('position', [ parsePercent$1(symbolOffset[0], symbolSize[0]), parsePercent$1(symbolOffset[1], symbolSize[1]) ]); } cursorStyle && symbolPath.attr('cursor', cursorStyle); // PENDING setColor before setStyle!!! symbolPath.setColor(color, seriesScope && seriesScope.symbolInnerColor); symbolPath.setStyle(itemStyle); var opacity = data.getItemVisual(idx, 'opacity'); if (opacity != null) { elStyle.opacity = opacity; } var liftZ = data.getItemVisual(idx, 'liftZ'); var z2Origin = symbolPath.__z2Origin; if (liftZ != null) { if (z2Origin == null) { symbolPath.__z2Origin = symbolPath.z2; symbolPath.z2 += liftZ; } } else if (z2Origin != null) { symbolPath.z2 = z2Origin; symbolPath.__z2Origin = null; } var useNameLabel = seriesScope && seriesScope.useNameLabel; setLabelStyle( elStyle, hoverItemStyle, labelModel, hoverLabelModel, { labelFetcher: seriesModel, labelDataIndex: idx, defaultText: getLabelDefaultText, isRectText: true, autoColor: color } ); // Do not execute util needed. function getLabelDefaultText(idx, opt) { return useNameLabel ? data.getName(idx) : getDefaultLabel(data, idx); } symbolPath.__symbolOriginalScale = getScale(symbolSize); symbolPath.hoverStyle = hoverItemStyle; symbolPath.highDownOnUpdate = ( hoverAnimation && seriesModel.isAnimationEnabled() ) ? highDownOnUpdate : null; setHoverStyle(symbolPath); }; function highDownOnUpdate(fromState, toState) { // Do not support this hover animation util some scenario required. // Animation can only be supported in hover layer when using `el.incremetal`. if (this.incremental || this.useHoverLayer) { return; } if (toState === 'emphasis') { var scale = this.__symbolOriginalScale; var ratio = scale[1] / scale[0]; var emphasisOpt = { scale: [ Math.max(scale[0] * 1.1, scale[0] + 3), Math.max(scale[1] * 1.1, scale[1] + 3 * ratio) ] }; // FIXME // modify it after support stop specified animation. // toState === fromState // ? (this.stopAnimation(), this.attr(emphasisOpt)) this.animateTo(emphasisOpt, 400, 'elasticOut'); } else if (toState === 'normal') { this.animateTo({ scale: this.__symbolOriginalScale }, 400, 'elasticOut'); } } /** * @param {Function} cb * @param {Object} [opt] * @param {Object} [opt.keepLabel=true] */ symbolProto.fadeOut = function (cb, opt) { var symbolPath = this.childAt(0); // Avoid mistaken hover when fading out this.silent = symbolPath.silent = true; // Not show text when animating !(opt && opt.keepLabel) && (symbolPath.style.text = null); updateProps( symbolPath, { style: {opacity: 0}, scale: [0, 0] }, this._seriesModel, this.dataIndex, cb ); }; inherits(SymbolClz$1, Group); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/chart/helper/SymbolDraw */ /** * @constructor * @alias module:echarts/chart/helper/SymbolDraw * @param {module:zrender/graphic/Group} [symbolCtor] */ function SymbolDraw(symbolCtor) { this.group = new Group(); this._symbolCtor = symbolCtor || SymbolClz$1; } var symbolDrawProto = SymbolDraw.prototype; function symbolNeedsDraw(data, point, idx, opt) { return point && !isNaN(point[0]) && !isNaN(point[1]) && !(opt.isIgnore && opt.isIgnore(idx)) // We do not set clipShape on group, because it will cut part of // the symbol element shape. We use the same clip shape here as // the line clip. && !(opt.clipShape && !opt.clipShape.contain(point[0], point[1])) && data.getItemVisual(idx, 'symbol') !== 'none'; } /** * Update symbols draw by new data * @param {module:echarts/data/List} data * @param {Object} [opt] Or isIgnore * @param {Function} [opt.isIgnore] * @param {Object} [opt.clipShape] */ symbolDrawProto.updateData = function (data, opt) { opt = normalizeUpdateOpt(opt); var group = this.group; var seriesModel = data.hostModel; var oldData = this._data; var SymbolCtor = this._symbolCtor; var seriesScope = makeSeriesScope(data); // There is no oldLineData only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!oldData) { group.removeAll(); } data.diff(oldData) .add(function (newIdx) { var point = data.getItemLayout(newIdx); if (symbolNeedsDraw(data, point, newIdx, opt)) { var symbolEl = new SymbolCtor(data, newIdx, seriesScope); symbolEl.attr('position', point); data.setItemGraphicEl(newIdx, symbolEl); group.add(symbolEl); } }) .update(function (newIdx, oldIdx) { var symbolEl = oldData.getItemGraphicEl(oldIdx); var point = data.getItemLayout(newIdx); if (!symbolNeedsDraw(data, point, newIdx, opt)) { group.remove(symbolEl); return; } if (!symbolEl) { symbolEl = new SymbolCtor(data, newIdx); symbolEl.attr('position', point); } else { symbolEl.updateData(data, newIdx, seriesScope); updateProps(symbolEl, { position: point }, seriesModel); } // Add back group.add(symbolEl); data.setItemGraphicEl(newIdx, symbolEl); }) .remove(function (oldIdx) { var el = oldData.getItemGraphicEl(oldIdx); el && el.fadeOut(function () { group.remove(el); }); }) .execute(); this._data = data; }; symbolDrawProto.isPersistent = function () { return true; }; symbolDrawProto.updateLayout = function () { var data = this._data; if (data) { // Not use animation data.eachItemGraphicEl(function (el, idx) { var point = data.getItemLayout(idx); el.attr('position', point); }); } }; symbolDrawProto.incrementalPrepareUpdate = function (data) { this._seriesScope = makeSeriesScope(data); this._data = null; this.group.removeAll(); }; /** * Update symbols draw by new data * @param {module:echarts/data/List} data * @param {Object} [opt] Or isIgnore * @param {Function} [opt.isIgnore] * @param {Object} [opt.clipShape] */ symbolDrawProto.incrementalUpdate = function (taskParams, data, opt) { opt = normalizeUpdateOpt(opt); function updateIncrementalAndHover(el) { if (!el.isGroup) { el.incremental = el.useHoverLayer = true; } } for (var idx = taskParams.start; idx < taskParams.end; idx++) { var point = data.getItemLayout(idx); if (symbolNeedsDraw(data, point, idx, opt)) { var el = new this._symbolCtor(data, idx, this._seriesScope); el.traverse(updateIncrementalAndHover); el.attr('position', point); this.group.add(el); data.setItemGraphicEl(idx, el); } } }; function normalizeUpdateOpt(opt) { if (opt != null && !isObject$1(opt)) { opt = {isIgnore: opt}; } return opt || {}; } symbolDrawProto.remove = function (enableAnimation) { var group = this.group; var data = this._data; // Incremental model do not have this._data. if (data && enableAnimation) { data.eachItemGraphicEl(function (el) { el.fadeOut(function () { group.remove(el); }); }); } else { group.removeAll(); } }; function makeSeriesScope(data) { var seriesModel = data.hostModel; return { itemStyle: seriesModel.getModel('itemStyle').getItemStyle(['color']), hoverItemStyle: seriesModel.getModel('emphasis.itemStyle').getItemStyle(), symbolRotate: seriesModel.get('symbolRotate'), symbolOffset: seriesModel.get('symbolOffset'), hoverAnimation: seriesModel.get('hoverAnimation'), labelModel: seriesModel.getModel('label'), hoverLabelModel: seriesModel.getModel('emphasis.label'), cursorStyle: seriesModel.get('cursor') }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {Object} coordSys * @param {module:echarts/data/List} data * @param {string} valueOrigin lineSeries.option.areaStyle.origin */ function prepareDataCoordInfo(coordSys, data, valueOrigin) { var baseAxis = coordSys.getBaseAxis(); var valueAxis = coordSys.getOtherAxis(baseAxis); var valueStart = getValueStart(valueAxis, valueOrigin); var baseAxisDim = baseAxis.dim; var valueAxisDim = valueAxis.dim; var valueDim = data.mapDimension(valueAxisDim); var baseDim = data.mapDimension(baseAxisDim); var baseDataOffset = valueAxisDim === 'x' || valueAxisDim === 'radius' ? 1 : 0; var dims = map(coordSys.dimensions, function (coordDim) { return data.mapDimension(coordDim); }); var stacked; var stackResultDim = data.getCalculationInfo('stackResultDimension'); if (stacked |= isDimensionStacked(data, dims[0] /*, dims[1]*/)) { // jshint ignore:line dims[0] = stackResultDim; } if (stacked |= isDimensionStacked(data, dims[1] /*, dims[0]*/)) { // jshint ignore:line dims[1] = stackResultDim; } return { dataDimsForPoint: dims, valueStart: valueStart, valueAxisDim: valueAxisDim, baseAxisDim: baseAxisDim, stacked: !!stacked, valueDim: valueDim, baseDim: baseDim, baseDataOffset: baseDataOffset, stackedOverDimension: data.getCalculationInfo('stackedOverDimension') }; } function getValueStart(valueAxis, valueOrigin) { var valueStart = 0; var extent = valueAxis.scale.getExtent(); if (valueOrigin === 'start') { valueStart = extent[0]; } else if (valueOrigin === 'end') { valueStart = extent[1]; } // auto else { // Both positive if (extent[0] > 0) { valueStart = extent[0]; } // Both negative else if (extent[1] < 0) { valueStart = extent[1]; } // If is one positive, and one negative, onZero shall be true } return valueStart; } function getStackedOnPoint(dataCoordInfo, coordSys, data, idx) { var value = NaN; if (dataCoordInfo.stacked) { value = data.get(data.getCalculationInfo('stackedOverDimension'), idx); } if (isNaN(value)) { value = dataCoordInfo.valueStart; } var baseDataOffset = dataCoordInfo.baseDataOffset; var stackedData = []; stackedData[baseDataOffset] = data.get(dataCoordInfo.baseDim, idx); stackedData[1 - baseDataOffset] = value; return coordSys.dataToPoint(stackedData); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // var arrayDiff = require('zrender/src/core/arrayDiff'); // 'zrender/src/core/arrayDiff' has been used before, but it did // not do well in performance when roam with fixed dataZoom window. // function convertToIntId(newIdList, oldIdList) { // // Generate int id instead of string id. // // Compare string maybe slow in score function of arrDiff // // Assume id in idList are all unique // var idIndicesMap = {}; // var idx = 0; // for (var i = 0; i < newIdList.length; i++) { // idIndicesMap[newIdList[i]] = idx; // newIdList[i] = idx++; // } // for (var i = 0; i < oldIdList.length; i++) { // var oldId = oldIdList[i]; // // Same with newIdList // if (idIndicesMap[oldId]) { // oldIdList[i] = idIndicesMap[oldId]; // } // else { // oldIdList[i] = idx++; // } // } // } function diffData(oldData, newData) { var diffResult = []; newData.diff(oldData) .add(function (idx) { diffResult.push({cmd: '+', idx: idx}); }) .update(function (newIdx, oldIdx) { diffResult.push({cmd: '=', idx: oldIdx, idx1: newIdx}); }) .remove(function (idx) { diffResult.push({cmd: '-', idx: idx}); }) .execute(); return diffResult; } var lineAnimationDiff = function ( oldData, newData, oldStackedOnPoints, newStackedOnPoints, oldCoordSys, newCoordSys, oldValueOrigin, newValueOrigin ) { var diff = diffData(oldData, newData); // var newIdList = newData.mapArray(newData.getId); // var oldIdList = oldData.mapArray(oldData.getId); // convertToIntId(newIdList, oldIdList); // // FIXME One data ? // diff = arrayDiff(oldIdList, newIdList); var currPoints = []; var nextPoints = []; // Points for stacking base line var currStackedPoints = []; var nextStackedPoints = []; var status = []; var sortedIndices = []; var rawIndices = []; var newDataOldCoordInfo = prepareDataCoordInfo(oldCoordSys, newData, oldValueOrigin); var oldDataNewCoordInfo = prepareDataCoordInfo(newCoordSys, oldData, newValueOrigin); for (var i = 0; i < diff.length; i++) { var diffItem = diff[i]; var pointAdded = true; // FIXME, animation is not so perfect when dataZoom window moves fast // Which is in case remvoing or add more than one data in the tail or head switch (diffItem.cmd) { case '=': var currentPt = oldData.getItemLayout(diffItem.idx); var nextPt = newData.getItemLayout(diffItem.idx1); // If previous data is NaN, use next point directly if (isNaN(currentPt[0]) || isNaN(currentPt[1])) { currentPt = nextPt.slice(); } currPoints.push(currentPt); nextPoints.push(nextPt); currStackedPoints.push(oldStackedOnPoints[diffItem.idx]); nextStackedPoints.push(newStackedOnPoints[diffItem.idx1]); rawIndices.push(newData.getRawIndex(diffItem.idx1)); break; case '+': var idx = diffItem.idx; currPoints.push( oldCoordSys.dataToPoint([ newData.get(newDataOldCoordInfo.dataDimsForPoint[0], idx), newData.get(newDataOldCoordInfo.dataDimsForPoint[1], idx) ]) ); nextPoints.push(newData.getItemLayout(idx).slice()); currStackedPoints.push( getStackedOnPoint(newDataOldCoordInfo, oldCoordSys, newData, idx) ); nextStackedPoints.push(newStackedOnPoints[idx]); rawIndices.push(newData.getRawIndex(idx)); break; case '-': var idx = diffItem.idx; var rawIndex = oldData.getRawIndex(idx); // Data is replaced. In the case of dynamic data queue // FIXME FIXME FIXME if (rawIndex !== idx) { currPoints.push(oldData.getItemLayout(idx)); nextPoints.push(newCoordSys.dataToPoint([ oldData.get(oldDataNewCoordInfo.dataDimsForPoint[0], idx), oldData.get(oldDataNewCoordInfo.dataDimsForPoint[1], idx) ])); currStackedPoints.push(oldStackedOnPoints[idx]); nextStackedPoints.push( getStackedOnPoint(oldDataNewCoordInfo, newCoordSys, oldData, idx) ); rawIndices.push(rawIndex); } else { pointAdded = false; } } // Original indices if (pointAdded) { status.push(diffItem); sortedIndices.push(sortedIndices.length); } } // Diff result may be crossed if all items are changed // Sort by data index sortedIndices.sort(function (a, b) { return rawIndices[a] - rawIndices[b]; }); var sortedCurrPoints = []; var sortedNextPoints = []; var sortedCurrStackedPoints = []; var sortedNextStackedPoints = []; var sortedStatus = []; for (var i = 0; i < sortedIndices.length; i++) { var idx = sortedIndices[i]; sortedCurrPoints[i] = currPoints[idx]; sortedNextPoints[i] = nextPoints[idx]; sortedCurrStackedPoints[i] = currStackedPoints[idx]; sortedNextStackedPoints[i] = nextStackedPoints[idx]; sortedStatus[i] = status[idx]; } return { current: sortedCurrPoints, next: sortedNextPoints, stackedOnCurrent: sortedCurrStackedPoints, stackedOnNext: sortedNextStackedPoints, status: sortedStatus }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Poly path support NaN point var vec2Min = min; var vec2Max = max; var scaleAndAdd$1 = scaleAndAdd; var v2Copy = copy; // Temporary variable var v = []; var cp0 = []; var cp1 = []; function isPointNull(p) { return isNaN(p[0]) || isNaN(p[1]); } function drawSegment( ctx, points, start, segLen, allLen, dir, smoothMin, smoothMax, smooth, smoothMonotone, connectNulls ) { // if (smoothMonotone == null) { // if (isMono(points, 'x')) { // return drawMono(ctx, points, start, segLen, allLen, // dir, smoothMin, smoothMax, smooth, 'x', connectNulls); // } // else if (isMono(points, 'y')) { // return drawMono(ctx, points, start, segLen, allLen, // dir, smoothMin, smoothMax, smooth, 'y', connectNulls); // } // else { // return drawNonMono.apply(this, arguments); // } // } // else if (smoothMonotone !== 'none' && isMono(points, smoothMonotone)) { // return drawMono.apply(this, arguments); // } // else { // return drawNonMono.apply(this, arguments); // } if (smoothMonotone === 'none' || !smoothMonotone) { return drawNonMono.apply(this, arguments); } else { return drawMono.apply(this, arguments); } } /** * Check if points is in monotone. * * @param {number[][]} points Array of points which is in [x, y] form * @param {string} smoothMonotone 'x', 'y', or 'none', stating for which * dimension that is checking. * If is 'none', `drawNonMono` should be * called. * If is undefined, either being monotone * in 'x' or 'y' will call `drawMono`. */ // function isMono(points, smoothMonotone) { // if (points.length <= 1) { // return true; // } // var dim = smoothMonotone === 'x' ? 0 : 1; // var last = points[0][dim]; // var lastDiff = 0; // for (var i = 1; i < points.length; ++i) { // var diff = points[i][dim] - last; // if (!isNaN(diff) && !isNaN(lastDiff) // && diff !== 0 && lastDiff !== 0 // && ((diff >= 0) !== (lastDiff >= 0)) // ) { // return false; // } // if (!isNaN(diff) && diff !== 0) { // lastDiff = diff; // last = points[i][dim]; // } // } // return true; // } /** * Draw smoothed line in monotone, in which only vertical or horizontal bezier * control points will be used. This should be used when points are monotone * either in x or y dimension. */ function drawMono( ctx, points, start, segLen, allLen, dir, smoothMin, smoothMax, smooth, smoothMonotone, connectNulls ) { var prevIdx = 0; var idx = start; for (var k = 0; k < segLen; k++) { var p = points[idx]; if (idx >= allLen || idx < 0) { break; } if (isPointNull(p)) { if (connectNulls) { idx += dir; continue; } break; } if (idx === start) { ctx[dir > 0 ? 'moveTo' : 'lineTo'](p[0], p[1]); } else { if (smooth > 0) { var prevP = points[prevIdx]; var dim = smoothMonotone === 'y' ? 1 : 0; // Length of control point to p, either in x or y, but not both var ctrlLen = (p[dim] - prevP[dim]) * smooth; v2Copy(cp0, prevP); cp0[dim] = prevP[dim] + ctrlLen; v2Copy(cp1, p); cp1[dim] = p[dim] - ctrlLen; ctx.bezierCurveTo( cp0[0], cp0[1], cp1[0], cp1[1], p[0], p[1] ); } else { ctx.lineTo(p[0], p[1]); } } prevIdx = idx; idx += dir; } return k; } /** * Draw smoothed line in non-monotone, in may cause undesired curve in extreme * situations. This should be used when points are non-monotone neither in x or * y dimension. */ function drawNonMono( ctx, points, start, segLen, allLen, dir, smoothMin, smoothMax, smooth, smoothMonotone, connectNulls ) { var prevIdx = 0; var idx = start; for (var k = 0; k < segLen; k++) { var p = points[idx]; if (idx >= allLen || idx < 0) { break; } if (isPointNull(p)) { if (connectNulls) { idx += dir; continue; } break; } if (idx === start) { ctx[dir > 0 ? 'moveTo' : 'lineTo'](p[0], p[1]); v2Copy(cp0, p); } else { if (smooth > 0) { var nextIdx = idx + dir; var nextP = points[nextIdx]; if (connectNulls) { // Find next point not null while (nextP && isPointNull(points[nextIdx])) { nextIdx += dir; nextP = points[nextIdx]; } } var ratioNextSeg = 0.5; var prevP = points[prevIdx]; var nextP = points[nextIdx]; // Last point if (!nextP || isPointNull(nextP)) { v2Copy(cp1, p); } else { // If next data is null in not connect case if (isPointNull(nextP) && !connectNulls) { nextP = p; } sub(v, nextP, prevP); var lenPrevSeg; var lenNextSeg; if (smoothMonotone === 'x' || smoothMonotone === 'y') { var dim = smoothMonotone === 'x' ? 0 : 1; lenPrevSeg = Math.abs(p[dim] - prevP[dim]); lenNextSeg = Math.abs(p[dim] - nextP[dim]); } else { lenPrevSeg = dist(p, prevP); lenNextSeg = dist(p, nextP); } // Use ratio of seg length ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg); scaleAndAdd$1(cp1, p, v, -smooth * (1 - ratioNextSeg)); } // Smooth constraint vec2Min(cp0, cp0, smoothMax); vec2Max(cp0, cp0, smoothMin); vec2Min(cp1, cp1, smoothMax); vec2Max(cp1, cp1, smoothMin); ctx.bezierCurveTo( cp0[0], cp0[1], cp1[0], cp1[1], p[0], p[1] ); // cp0 of next segment scaleAndAdd$1(cp0, p, v, smooth * ratioNextSeg); } else { ctx.lineTo(p[0], p[1]); } } prevIdx = idx; idx += dir; } return k; } function getBoundingBox(points, smoothConstraint) { var ptMin = [Infinity, Infinity]; var ptMax = [-Infinity, -Infinity]; if (smoothConstraint) { for (var i = 0; i < points.length; i++) { var pt = points[i]; if (pt[0] < ptMin[0]) { ptMin[0] = pt[0]; } if (pt[1] < ptMin[1]) { ptMin[1] = pt[1]; } if (pt[0] > ptMax[0]) { ptMax[0] = pt[0]; } if (pt[1] > ptMax[1]) { ptMax[1] = pt[1]; } } } return { min: smoothConstraint ? ptMin : ptMax, max: smoothConstraint ? ptMax : ptMin }; } var Polyline$1 = Path.extend({ type: 'ec-polyline', shape: { points: [], smooth: 0, smoothConstraint: true, smoothMonotone: null, connectNulls: false }, style: { fill: null, stroke: '#000' }, brush: fixClipWithShadow(Path.prototype.brush), buildPath: function (ctx, shape) { var points = shape.points; var i = 0; var len$$1 = points.length; var result = getBoundingBox(points, shape.smoothConstraint); if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len$$1 > 0; len$$1--) { if (!isPointNull(points[len$$1 - 1])) { break; } } for (; i < len$$1; i++) { if (!isPointNull(points[i])) { break; } } } while (i < len$$1) { i += drawSegment( ctx, points, i, len$$1, len$$1, 1, result.min, result.max, shape.smooth, shape.smoothMonotone, shape.connectNulls ) + 1; } } }); var Polygon$1 = Path.extend({ type: 'ec-polygon', shape: { points: [], // Offset between stacked base points and points stackedOnPoints: [], smooth: 0, stackedOnSmooth: 0, smoothConstraint: true, smoothMonotone: null, connectNulls: false }, brush: fixClipWithShadow(Path.prototype.brush), buildPath: function (ctx, shape) { var points = shape.points; var stackedOnPoints = shape.stackedOnPoints; var i = 0; var len$$1 = points.length; var smoothMonotone = shape.smoothMonotone; var bbox = getBoundingBox(points, shape.smoothConstraint); var stackedOnBBox = getBoundingBox(stackedOnPoints, shape.smoothConstraint); if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len$$1 > 0; len$$1--) { if (!isPointNull(points[len$$1 - 1])) { break; } } for (; i < len$$1; i++) { if (!isPointNull(points[i])) { break; } } } while (i < len$$1) { var k = drawSegment( ctx, points, i, len$$1, len$$1, 1, bbox.min, bbox.max, shape.smooth, smoothMonotone, shape.connectNulls ); drawSegment( ctx, stackedOnPoints, i + k - 1, k, len$$1, -1, stackedOnBBox.min, stackedOnBBox.max, shape.stackedOnSmooth, smoothMonotone, shape.connectNulls ); i += k + 1; ctx.closePath(); } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function createGridClipPath(cartesian, hasAnimation, seriesModel) { var rect = cartesian.getArea(); var isHorizontal = cartesian.getBaseAxis().isHorizontal(); var x = rect.x; var y = rect.y; var width = rect.width; var height = rect.height; var lineWidth = seriesModel.get('lineStyle.width') || 2; // Expand the clip path a bit to avoid the border is clipped and looks thinner x -= lineWidth / 2; y -= lineWidth / 2; width += lineWidth; height += lineWidth; // fix: https://github.com/apache/incubator-echarts/issues/11369 x = Math.floor(x); width = Math.round(width); var clipPath = new Rect({ shape: { x: x, y: y, width: width, height: height } }); if (hasAnimation) { clipPath.shape[isHorizontal ? 'width' : 'height'] = 0; initProps(clipPath, { shape: { width: width, height: height } }, seriesModel); } return clipPath; } function createPolarClipPath(polar, hasAnimation, seriesModel) { var sectorArea = polar.getArea(); // Avoid float number rounding error for symbol on the edge of axis extent. var clipPath = new Sector({ shape: { cx: round$1(polar.cx, 1), cy: round$1(polar.cy, 1), r0: round$1(sectorArea.r0, 1), r: round$1(sectorArea.r, 1), startAngle: sectorArea.startAngle, endAngle: sectorArea.endAngle, clockwise: sectorArea.clockwise } }); if (hasAnimation) { clipPath.shape.endAngle = sectorArea.startAngle; initProps(clipPath, { shape: { endAngle: sectorArea.endAngle } }, seriesModel); } return clipPath; } function createClipPath(coordSys, hasAnimation, seriesModel) { if (!coordSys) { return null; } else if (coordSys.type === 'polar') { return createPolarClipPath(coordSys, hasAnimation, seriesModel); } else if (coordSys.type === 'cartesian2d') { return createGridClipPath(coordSys, hasAnimation, seriesModel); } return null; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // FIXME step not support polar function isPointsSame(points1, points2) { if (points1.length !== points2.length) { return; } for (var i = 0; i < points1.length; i++) { var p1 = points1[i]; var p2 = points2[i]; if (p1[0] !== p2[0] || p1[1] !== p2[1]) { return; } } return true; } function getBoundingDiff(points1, points2) { var min1 = []; var max1 = []; var min2 = []; var max2 = []; fromPoints(points1, min1, max1); fromPoints(points2, min2, max2); // Get a max value from each corner of two boundings. return Math.max( Math.abs(min1[0] - min2[0]), Math.abs(min1[1] - min2[1]), Math.abs(max1[0] - max2[0]), Math.abs(max1[1] - max2[1]) ); } function getSmooth(smooth) { return typeof (smooth) === 'number' ? smooth : (smooth ? 0.5 : 0); } /** * @param {module:echarts/coord/cartesian/Cartesian2D|module:echarts/coord/polar/Polar} coordSys * @param {module:echarts/data/List} data * @param {Object} dataCoordInfo * @param {Array.>} points */ function getStackedOnPoints(coordSys, data, dataCoordInfo) { if (!dataCoordInfo.valueDim) { return []; } var points = []; for (var idx = 0, len = data.count(); idx < len; idx++) { points.push(getStackedOnPoint(dataCoordInfo, coordSys, data, idx)); } return points; } function turnPointsIntoStep(points, coordSys, stepTurnAt) { var baseAxis = coordSys.getBaseAxis(); var baseIndex = baseAxis.dim === 'x' || baseAxis.dim === 'radius' ? 0 : 1; var stepPoints = []; for (var i = 0; i < points.length - 1; i++) { var nextPt = points[i + 1]; var pt = points[i]; stepPoints.push(pt); var stepPt = []; switch (stepTurnAt) { case 'end': stepPt[baseIndex] = nextPt[baseIndex]; stepPt[1 - baseIndex] = pt[1 - baseIndex]; // default is start stepPoints.push(stepPt); break; case 'middle': // default is start var middle = (pt[baseIndex] + nextPt[baseIndex]) / 2; var stepPt2 = []; stepPt[baseIndex] = stepPt2[baseIndex] = middle; stepPt[1 - baseIndex] = pt[1 - baseIndex]; stepPt2[1 - baseIndex] = nextPt[1 - baseIndex]; stepPoints.push(stepPt); stepPoints.push(stepPt2); break; default: stepPt[baseIndex] = pt[baseIndex]; stepPt[1 - baseIndex] = nextPt[1 - baseIndex]; // default is start stepPoints.push(stepPt); } } // Last points points[i] && stepPoints.push(points[i]); return stepPoints; } function getVisualGradient(data, coordSys) { var visualMetaList = data.getVisual('visualMeta'); if (!visualMetaList || !visualMetaList.length || !data.count()) { // When data.count() is 0, gradient range can not be calculated. return; } if (coordSys.type !== 'cartesian2d') { if (__DEV__) { console.warn('Visual map on line style is only supported on cartesian2d.'); } return; } var coordDim; var visualMeta; for (var i = visualMetaList.length - 1; i >= 0; i--) { var dimIndex = visualMetaList[i].dimension; var dimName = data.dimensions[dimIndex]; var dimInfo = data.getDimensionInfo(dimName); coordDim = dimInfo && dimInfo.coordDim; // Can only be x or y if (coordDim === 'x' || coordDim === 'y') { visualMeta = visualMetaList[i]; break; } } if (!visualMeta) { if (__DEV__) { console.warn('Visual map on line style only support x or y dimension.'); } return; } // If the area to be rendered is bigger than area defined by LinearGradient, // the canvas spec prescribes that the color of the first stop and the last // stop should be used. But if two stops are added at offset 0, in effect // browsers use the color of the second stop to render area outside // LinearGradient. So we can only infinitesimally extend area defined in // LinearGradient to render `outerColors`. var axis = coordSys.getAxis(coordDim); // dataToCoor mapping may not be linear, but must be monotonic. var colorStops = map(visualMeta.stops, function (stop) { return { coord: axis.toGlobalCoord(axis.dataToCoord(stop.value)), color: stop.color }; }); var stopLen = colorStops.length; var outerColors = visualMeta.outerColors.slice(); if (stopLen && colorStops[0].coord > colorStops[stopLen - 1].coord) { colorStops.reverse(); outerColors.reverse(); } var tinyExtent = 10; // Arbitrary value: 10px var minCoord = colorStops[0].coord - tinyExtent; var maxCoord = colorStops[stopLen - 1].coord + tinyExtent; var coordSpan = maxCoord - minCoord; if (coordSpan < 1e-3) { return 'transparent'; } each$1(colorStops, function (stop) { stop.offset = (stop.coord - minCoord) / coordSpan; }); colorStops.push({ offset: stopLen ? colorStops[stopLen - 1].offset : 0.5, color: outerColors[1] || 'transparent' }); colorStops.unshift({ // notice colorStops.length have been changed. offset: stopLen ? colorStops[0].offset : 0.5, color: outerColors[0] || 'transparent' }); // zrUtil.each(colorStops, function (colorStop) { // // Make sure each offset has rounded px to avoid not sharp edge // colorStop.offset = (Math.round(colorStop.offset * (end - start) + start) - start) / (end - start); // }); var gradient = new LinearGradient(0, 0, 0, 0, colorStops, true); gradient[coordDim] = minCoord; gradient[coordDim + '2'] = maxCoord; return gradient; } function getIsIgnoreFunc(seriesModel, data, coordSys) { var showAllSymbol = seriesModel.get('showAllSymbol'); var isAuto = showAllSymbol === 'auto'; if (showAllSymbol && !isAuto) { return; } var categoryAxis = coordSys.getAxesByScale('ordinal')[0]; if (!categoryAxis) { return; } // Note that category label interval strategy might bring some weird effect // in some scenario: users may wonder why some of the symbols are not // displayed. So we show all symbols as possible as we can. if (isAuto // Simplify the logic, do not determine label overlap here. && canShowAllSymbolForCategory(categoryAxis, data) ) { return; } // Otherwise follow the label interval strategy on category axis. var categoryDataDim = data.mapDimension(categoryAxis.dim); var labelMap = {}; each$1(categoryAxis.getViewLabels(), function (labelItem) { labelMap[labelItem.tickValue] = 1; }); return function (dataIndex) { return !labelMap.hasOwnProperty(data.get(categoryDataDim, dataIndex)); }; } function canShowAllSymbolForCategory(categoryAxis, data) { // In mose cases, line is monotonous on category axis, and the label size // is close with each other. So we check the symbol size and some of the // label size alone with the category axis to estimate whether all symbol // can be shown without overlap. var axisExtent = categoryAxis.getExtent(); var availSize = Math.abs(axisExtent[1] - axisExtent[0]) / categoryAxis.scale.count(); isNaN(availSize) && (availSize = 0); // 0/0 is NaN. // Sampling some points, max 5. var dataLen = data.count(); var step = Math.max(1, Math.round(dataLen / 5)); for (var dataIndex = 0; dataIndex < dataLen; dataIndex += step) { if (SymbolClz$1.getSymbolSize( data, dataIndex // Only for cartesian, where `isHorizontal` exists. )[categoryAxis.isHorizontal() ? 1 : 0] // Empirical number * 1.5 > availSize ) { return false; } } return true; } function createLineClipPath(coordSys, hasAnimation, seriesModel) { if (coordSys.type === 'cartesian2d') { var isHorizontal = coordSys.getBaseAxis().isHorizontal(); var clipPath = createGridClipPath(coordSys, hasAnimation, seriesModel); // Expand clip shape to avoid clipping when line value exceeds axis if (!seriesModel.get('clip', true)) { var rectShape = clipPath.shape; var expandSize = Math.max(rectShape.width, rectShape.height); if (isHorizontal) { rectShape.y -= expandSize; rectShape.height += expandSize * 2; } else { rectShape.x -= expandSize; rectShape.width += expandSize * 2; } } return clipPath; } else { return createPolarClipPath(coordSys, hasAnimation, seriesModel); } } Chart.extend({ type: 'line', init: function () { var lineGroup = new Group(); var symbolDraw = new SymbolDraw(); this.group.add(symbolDraw.group); this._symbolDraw = symbolDraw; this._lineGroup = lineGroup; }, render: function (seriesModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var group = this.group; var data = seriesModel.getData(); var lineStyleModel = seriesModel.getModel('lineStyle'); var areaStyleModel = seriesModel.getModel('areaStyle'); var points = data.mapArray(data.getItemLayout); var isCoordSysPolar = coordSys.type === 'polar'; var prevCoordSys = this._coordSys; var symbolDraw = this._symbolDraw; var polyline = this._polyline; var polygon = this._polygon; var lineGroup = this._lineGroup; var hasAnimation = seriesModel.get('animation'); var isAreaChart = !areaStyleModel.isEmpty(); var valueOrigin = areaStyleModel.get('origin'); var dataCoordInfo = prepareDataCoordInfo(coordSys, data, valueOrigin); var stackedOnPoints = getStackedOnPoints(coordSys, data, dataCoordInfo); var showSymbol = seriesModel.get('showSymbol'); var isIgnoreFunc = showSymbol && !isCoordSysPolar && getIsIgnoreFunc(seriesModel, data, coordSys); // Remove temporary symbols var oldData = this._data; oldData && oldData.eachItemGraphicEl(function (el, idx) { if (el.__temp) { group.remove(el); oldData.setItemGraphicEl(idx, null); } }); // Remove previous created symbols if showSymbol changed to false if (!showSymbol) { symbolDraw.remove(); } group.add(lineGroup); // FIXME step not support polar var step = !isCoordSysPolar && seriesModel.get('step'); var clipShapeForSymbol; if (coordSys && coordSys.getArea && seriesModel.get('clip', true)) { clipShapeForSymbol = coordSys.getArea(); // Avoid float number rounding error for symbol on the edge of axis extent. // See #7913 and `test/dataZoom-clip.html`. if (clipShapeForSymbol.width != null) { clipShapeForSymbol.x -= 0.1; clipShapeForSymbol.y -= 0.1; clipShapeForSymbol.width += 0.2; clipShapeForSymbol.height += 0.2; } else if (clipShapeForSymbol.r0) { clipShapeForSymbol.r0 -= 0.5; clipShapeForSymbol.r1 += 0.5; } } this._clipShapeForSymbol = clipShapeForSymbol; // Initialization animation or coordinate system changed if ( !(polyline && prevCoordSys.type === coordSys.type && step === this._step) ) { showSymbol && symbolDraw.updateData(data, { isIgnore: isIgnoreFunc, clipShape: clipShapeForSymbol }); if (step) { // TODO If stacked series is not step points = turnPointsIntoStep(points, coordSys, step); stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step); } polyline = this._newPolyline(points, coordSys, hasAnimation); if (isAreaChart) { polygon = this._newPolygon( points, stackedOnPoints, coordSys, hasAnimation ); } lineGroup.setClipPath(createLineClipPath(coordSys, true, seriesModel)); } else { if (isAreaChart && !polygon) { // If areaStyle is added polygon = this._newPolygon( points, stackedOnPoints, coordSys, hasAnimation ); } else if (polygon && !isAreaChart) { // If areaStyle is removed lineGroup.remove(polygon); polygon = this._polygon = null; } // Update clipPath lineGroup.setClipPath(createLineClipPath(coordSys, false, seriesModel)); // Always update, or it is wrong in the case turning on legend // because points are not changed showSymbol && symbolDraw.updateData(data, { isIgnore: isIgnoreFunc, clipShape: clipShapeForSymbol }); // Stop symbol animation and sync with line points // FIXME performance? data.eachItemGraphicEl(function (el) { el.stopAnimation(true); }); // In the case data zoom triggerred refreshing frequently // Data may not change if line has a category axis. So it should animate nothing if (!isPointsSame(this._stackedOnPoints, stackedOnPoints) || !isPointsSame(this._points, points) ) { if (hasAnimation) { this._updateAnimation( data, stackedOnPoints, coordSys, api, step, valueOrigin ); } else { // Not do it in update with animation if (step) { // TODO If stacked series is not step points = turnPointsIntoStep(points, coordSys, step); stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step); } polyline.setShape({ points: points }); polygon && polygon.setShape({ points: points, stackedOnPoints: stackedOnPoints }); } } } var visualColor = getVisualGradient(data, coordSys) || data.getVisual('color'); polyline.useStyle(defaults( // Use color in lineStyle first lineStyleModel.getLineStyle(), { fill: 'none', stroke: visualColor, lineJoin: 'bevel' } )); var smooth = seriesModel.get('smooth'); smooth = getSmooth(seriesModel.get('smooth')); polyline.setShape({ smooth: smooth, smoothMonotone: seriesModel.get('smoothMonotone'), connectNulls: seriesModel.get('connectNulls') }); if (polygon) { var stackedOnSeries = data.getCalculationInfo('stackedOnSeries'); var stackedOnSmooth = 0; polygon.useStyle(defaults( areaStyleModel.getAreaStyle(), { fill: visualColor, opacity: 0.7, lineJoin: 'bevel' } )); if (stackedOnSeries) { stackedOnSmooth = getSmooth(stackedOnSeries.get('smooth')); } polygon.setShape({ smooth: smooth, stackedOnSmooth: stackedOnSmooth, smoothMonotone: seriesModel.get('smoothMonotone'), connectNulls: seriesModel.get('connectNulls') }); } this._data = data; // Save the coordinate system for transition animation when data changed this._coordSys = coordSys; this._stackedOnPoints = stackedOnPoints; this._points = points; this._step = step; this._valueOrigin = valueOrigin; }, dispose: function () {}, highlight: function (seriesModel, ecModel, api, payload) { var data = seriesModel.getData(); var dataIndex = queryDataIndex(data, payload); if (!(dataIndex instanceof Array) && dataIndex != null && dataIndex >= 0) { var symbol = data.getItemGraphicEl(dataIndex); if (!symbol) { // Create a temporary symbol if it is not exists var pt = data.getItemLayout(dataIndex); if (!pt) { // Null data return; } // fix #11360: should't draw symbol outside clipShapeForSymbol if (this._clipShapeForSymbol && !this._clipShapeForSymbol.contain(pt[0], pt[1])) { return; } symbol = new SymbolClz$1(data, dataIndex); symbol.position = pt; symbol.setZ( seriesModel.get('zlevel'), seriesModel.get('z') ); symbol.ignore = isNaN(pt[0]) || isNaN(pt[1]); symbol.__temp = true; data.setItemGraphicEl(dataIndex, symbol); // Stop scale animation symbol.stopSymbolAnimation(true); this.group.add(symbol); } symbol.highlight(); } else { // Highlight whole series Chart.prototype.highlight.call( this, seriesModel, ecModel, api, payload ); } }, downplay: function (seriesModel, ecModel, api, payload) { var data = seriesModel.getData(); var dataIndex = queryDataIndex(data, payload); if (dataIndex != null && dataIndex >= 0) { var symbol = data.getItemGraphicEl(dataIndex); if (symbol) { if (symbol.__temp) { data.setItemGraphicEl(dataIndex, null); this.group.remove(symbol); } else { symbol.downplay(); } } } else { // FIXME // can not downplay completely. // Downplay whole series Chart.prototype.downplay.call( this, seriesModel, ecModel, api, payload ); } }, /** * @param {module:zrender/container/Group} group * @param {Array.>} points * @private */ _newPolyline: function (points) { var polyline = this._polyline; // Remove previous created polyline if (polyline) { this._lineGroup.remove(polyline); } polyline = new Polyline$1({ shape: { points: points }, silent: true, z2: 10 }); this._lineGroup.add(polyline); this._polyline = polyline; return polyline; }, /** * @param {module:zrender/container/Group} group * @param {Array.>} stackedOnPoints * @param {Array.>} points * @private */ _newPolygon: function (points, stackedOnPoints) { var polygon = this._polygon; // Remove previous created polygon if (polygon) { this._lineGroup.remove(polygon); } polygon = new Polygon$1({ shape: { points: points, stackedOnPoints: stackedOnPoints }, silent: true }); this._lineGroup.add(polygon); this._polygon = polygon; return polygon; }, /** * @private */ // FIXME Two value axis _updateAnimation: function (data, stackedOnPoints, coordSys, api, step, valueOrigin) { var polyline = this._polyline; var polygon = this._polygon; var seriesModel = data.hostModel; var diff = lineAnimationDiff( this._data, data, this._stackedOnPoints, stackedOnPoints, this._coordSys, coordSys, this._valueOrigin, valueOrigin ); var current = diff.current; var stackedOnCurrent = diff.stackedOnCurrent; var next = diff.next; var stackedOnNext = diff.stackedOnNext; if (step) { // TODO If stacked series is not step current = turnPointsIntoStep(diff.current, coordSys, step); stackedOnCurrent = turnPointsIntoStep(diff.stackedOnCurrent, coordSys, step); next = turnPointsIntoStep(diff.next, coordSys, step); stackedOnNext = turnPointsIntoStep(diff.stackedOnNext, coordSys, step); } // Don't apply animation if diff is large. // For better result and avoid memory explosion problems like // https://github.com/apache/incubator-echarts/issues/12229 if (getBoundingDiff(current, next) > 3000 || (polygon && getBoundingDiff(stackedOnCurrent, stackedOnNext) > 3000) ) { polyline.setShape({ points: next }); if (polygon) { polygon.setShape({ points: next, stackedOnPoints: stackedOnNext }); } return; } // `diff.current` is subset of `current` (which should be ensured by // turnPointsIntoStep), so points in `__points` can be updated when // points in `current` are update during animation. polyline.shape.__points = diff.current; polyline.shape.points = current; updateProps(polyline, { shape: { points: next } }, seriesModel); if (polygon) { polygon.setShape({ points: current, stackedOnPoints: stackedOnCurrent }); updateProps(polygon, { shape: { points: next, stackedOnPoints: stackedOnNext } }, seriesModel); } var updatedDataInfo = []; var diffStatus = diff.status; for (var i = 0; i < diffStatus.length; i++) { var cmd = diffStatus[i].cmd; if (cmd === '=') { var el = data.getItemGraphicEl(diffStatus[i].idx1); if (el) { updatedDataInfo.push({ el: el, ptIdx: i // Index of points }); } } } if (polyline.animators && polyline.animators.length) { polyline.animators[0].during(function () { for (var i = 0; i < updatedDataInfo.length; i++) { var el = updatedDataInfo[i].el; el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]); } }); } }, remove: function (ecModel) { var group = this.group; var oldData = this._data; this._lineGroup.removeAll(); this._symbolDraw.remove(true); // Remove temporary created elements when highlighting oldData && oldData.eachItemGraphicEl(function (el, idx) { if (el.__temp) { group.remove(el); oldData.setItemGraphicEl(idx, null); } }); this._polyline = this._polygon = this._coordSys = this._points = this._stackedOnPoints = this._data = null; } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var visualSymbol = function (seriesType, defaultSymbolType, legendSymbol) { // Encoding visual for all series include which is filtered for legend drawing return { seriesType: seriesType, // For legend. performRawSeries: true, reset: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var symbolType = seriesModel.get('symbol'); var symbolSize = seriesModel.get('symbolSize'); var keepAspect = seriesModel.get('symbolKeepAspect'); var symbolRotate = seriesModel.get('symbolRotate'); var hasSymbolTypeCallback = isFunction$1(symbolType); var hasSymbolSizeCallback = isFunction$1(symbolSize); var hasSymbolRotateCallback = isFunction$1(symbolRotate); var hasCallback = hasSymbolTypeCallback || hasSymbolSizeCallback || hasSymbolRotateCallback; var seriesSymbol = (!hasSymbolTypeCallback && symbolType) ? symbolType : defaultSymbolType; var seriesSymbolSize = !hasSymbolSizeCallback ? symbolSize : null; data.setVisual({ legendSymbol: legendSymbol || seriesSymbol, // If seting callback functions on `symbol` or `symbolSize`, for simplicity and avoiding // to bring trouble, we do not pick a reuslt from one of its calling on data item here, // but just use the default value. Callback on `symbol` or `symbolSize` is convenient in // some cases but generally it is not recommanded. symbol: seriesSymbol, symbolSize: seriesSymbolSize, symbolKeepAspect: keepAspect, symbolRotate: symbolRotate }); // Only visible series has each data be visual encoded if (ecModel.isSeriesFiltered(seriesModel)) { return; } function dataEach(data, idx) { if (hasCallback) { var rawValue = seriesModel.getRawValue(idx); var params = seriesModel.getDataParams(idx); hasSymbolTypeCallback && data.setItemVisual(idx, 'symbol', symbolType(rawValue, params)); hasSymbolSizeCallback && data.setItemVisual(idx, 'symbolSize', symbolSize(rawValue, params)); hasSymbolRotateCallback && data.setItemVisual(idx, 'symbolRotate', symbolRotate(rawValue, params)); } if (data.hasItemOption) { var itemModel = data.getItemModel(idx); var itemSymbolType = itemModel.getShallow('symbol', true); var itemSymbolSize = itemModel.getShallow('symbolSize', true); var itemSymbolRotate = itemModel.getShallow('symbolRotate', true); var itemSymbolKeepAspect = itemModel.getShallow('symbolKeepAspect', true); // If has item symbol if (itemSymbolType != null) { data.setItemVisual(idx, 'symbol', itemSymbolType); } if (itemSymbolSize != null) { // PENDING Transform symbolSize ? data.setItemVisual(idx, 'symbolSize', itemSymbolSize); } if (itemSymbolRotate != null) { data.setItemVisual(idx, 'symbolRotate', itemSymbolRotate); } if (itemSymbolKeepAspect != null) { data.setItemVisual(idx, 'symbolKeepAspect', itemSymbolKeepAspect); } } } return { dataEach: (data.hasItemOption || hasCallback) ? dataEach : null }; } }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global Float32Array */ var pointsLayout = function (seriesType) { return { seriesType: seriesType, plan: createRenderPlanner(), reset: function (seriesModel) { var data = seriesModel.getData(); var coordSys = seriesModel.coordinateSystem; var pipelineContext = seriesModel.pipelineContext; var isLargeRender = pipelineContext.large; if (!coordSys) { return; } var dims = map(coordSys.dimensions, function (dim) { return data.mapDimension(dim); }).slice(0, 2); var dimLen = dims.length; var stackResultDim = data.getCalculationInfo('stackResultDimension'); if (isDimensionStacked(data, dims[0] /*, dims[1]*/)) { dims[0] = stackResultDim; } if (isDimensionStacked(data, dims[1] /*, dims[0]*/)) { dims[1] = stackResultDim; } function progress(params, data) { var segCount = params.end - params.start; var points = isLargeRender && new Float32Array(segCount * dimLen); for (var i = params.start, offset = 0, tmpIn = [], tmpOut = []; i < params.end; i++) { var point; if (dimLen === 1) { var x = data.get(dims[0], i); point = !isNaN(x) && coordSys.dataToPoint(x, null, tmpOut); } else { var x = tmpIn[0] = data.get(dims[0], i); var y = tmpIn[1] = data.get(dims[1], i); // Also {Array.}, not undefined to avoid if...else... statement point = !isNaN(x) && !isNaN(y) && coordSys.dataToPoint(tmpIn, null, tmpOut); } if (isLargeRender) { points[offset++] = point ? point[0] : NaN; points[offset++] = point ? point[1] : NaN; } else { data.setItemLayout(i, (point && point.slice()) || [NaN, NaN]); } } isLargeRender && data.setLayout('symbolPoints', points); } return dimLen && {progress: progress}; } }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var samplers = { average: function (frame) { var sum = 0; var count = 0; for (var i = 0; i < frame.length; i++) { if (!isNaN(frame[i])) { sum += frame[i]; count++; } } // Return NaN if count is 0 return count === 0 ? NaN : sum / count; }, sum: function (frame) { var sum = 0; for (var i = 0; i < frame.length; i++) { // Ignore NaN sum += frame[i] || 0; } return sum; }, max: function (frame) { var max = -Infinity; for (var i = 0; i < frame.length; i++) { frame[i] > max && (max = frame[i]); } // NaN will cause illegal axis extent. return isFinite(max) ? max : NaN; }, min: function (frame) { var min = Infinity; for (var i = 0; i < frame.length; i++) { frame[i] < min && (min = frame[i]); } // NaN will cause illegal axis extent. return isFinite(min) ? min : NaN; }, // TODO // Median nearest: function (frame) { return frame[0]; } }; var indexSampler = function (frame, value) { return Math.round(frame.length / 2); }; var dataSample = function (seriesType) { return { seriesType: seriesType, modifyOutputEnd: true, reset: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var sampling = seriesModel.get('sampling'); var coordSys = seriesModel.coordinateSystem; // Only cartesian2d support down sampling if (coordSys.type === 'cartesian2d' && sampling) { var baseAxis = coordSys.getBaseAxis(); var valueAxis = coordSys.getOtherAxis(baseAxis); var extent = baseAxis.getExtent(); // Coordinste system has been resized var size = Math.abs(extent[1] - extent[0]); var rate = Math.round(data.count() / size); if (rate > 1) { var sampler; if (typeof sampling === 'string') { sampler = samplers[sampling]; } else if (typeof sampling === 'function') { sampler = sampling; } if (sampler) { // Only support sample the first dim mapped from value axis. seriesModel.setData(data.downSample( data.mapDimension(valueAxis.dim), 1 / rate, sampler, indexSampler )); } } } } }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Cartesian coordinate system * @module echarts/coord/Cartesian * */ function dimAxisMapper(dim) { return this._axes[dim]; } /** * @alias module:echarts/coord/Cartesian * @constructor */ var Cartesian = function (name) { this._axes = {}; this._dimList = []; /** * @type {string} */ this.name = name || ''; }; Cartesian.prototype = { constructor: Cartesian, type: 'cartesian', /** * Get axis * @param {number|string} dim * @return {module:echarts/coord/Cartesian~Axis} */ getAxis: function (dim) { return this._axes[dim]; }, /** * Get axes list * @return {Array.} */ getAxes: function () { return map(this._dimList, dimAxisMapper, this); }, /** * Get axes list by given scale type */ getAxesByScale: function (scaleType) { scaleType = scaleType.toLowerCase(); return filter( this.getAxes(), function (axis) { return axis.scale.type === scaleType; } ); }, /** * Add axis * @param {module:echarts/coord/Cartesian.Axis} */ addAxis: function (axis) { var dim = axis.dim; this._axes[dim] = axis; this._dimList.push(dim); }, /** * Convert data to coord in nd space * @param {Array.|Object.} val * @return {Array.|Object.} */ dataToCoord: function (val) { return this._dataCoordConvert(val, 'dataToCoord'); }, /** * Convert coord in nd space to data * @param {Array.|Object.} val * @return {Array.|Object.} */ coordToData: function (val) { return this._dataCoordConvert(val, 'coordToData'); }, _dataCoordConvert: function (input, method) { var dimList = this._dimList; var output = input instanceof Array ? [] : {}; for (var i = 0; i < dimList.length; i++) { var dim = dimList[i]; var axis = this._axes[dim]; output[dim] = axis[method](input[dim]); } return output; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function Cartesian2D(name) { Cartesian.call(this, name); } Cartesian2D.prototype = { constructor: Cartesian2D, type: 'cartesian2d', /** * @type {Array.} * @readOnly */ dimensions: ['x', 'y'], /** * Base axis will be used on stacking. * * @return {module:echarts/coord/cartesian/Axis2D} */ getBaseAxis: function () { return this.getAxesByScale('ordinal')[0] || this.getAxesByScale('time')[0] || this.getAxis('x'); }, /** * If contain point * @param {Array.} point * @return {boolean} */ containPoint: function (point) { var axisX = this.getAxis('x'); var axisY = this.getAxis('y'); return axisX.contain(axisX.toLocalCoord(point[0])) && axisY.contain(axisY.toLocalCoord(point[1])); }, /** * If contain data * @param {Array.} data * @return {boolean} */ containData: function (data) { return this.getAxis('x').containData(data[0]) && this.getAxis('y').containData(data[1]); }, /** * @param {Array.} data * @param {Array.} out * @return {Array.} */ dataToPoint: function (data, reserved, out) { var xAxis = this.getAxis('x'); var yAxis = this.getAxis('y'); out = out || []; out[0] = xAxis.toGlobalCoord(xAxis.dataToCoord(data[0])); out[1] = yAxis.toGlobalCoord(yAxis.dataToCoord(data[1])); return out; }, /** * @param {Array.} data * @param {Array.} out * @return {Array.} */ clampData: function (data, out) { var xScale = this.getAxis('x').scale; var yScale = this.getAxis('y').scale; var xAxisExtent = xScale.getExtent(); var yAxisExtent = yScale.getExtent(); var x = xScale.parse(data[0]); var y = yScale.parse(data[1]); out = out || []; out[0] = Math.min( Math.max(Math.min(xAxisExtent[0], xAxisExtent[1]), x), Math.max(xAxisExtent[0], xAxisExtent[1]) ); out[1] = Math.min( Math.max(Math.min(yAxisExtent[0], yAxisExtent[1]), y), Math.max(yAxisExtent[0], yAxisExtent[1]) ); return out; }, /** * @param {Array.} point * @param {Array.} out * @return {Array.} */ pointToData: function (point, out) { var xAxis = this.getAxis('x'); var yAxis = this.getAxis('y'); out = out || []; out[0] = xAxis.coordToData(xAxis.toLocalCoord(point[0])); out[1] = yAxis.coordToData(yAxis.toLocalCoord(point[1])); return out; }, /** * Get other axis * @param {module:echarts/coord/cartesian/Axis2D} axis */ getOtherAxis: function (axis) { return this.getAxis(axis.dim === 'x' ? 'y' : 'x'); }, /** * Get rect area of cartesian. * Area will have a contain function to determine if a point is in the coordinate system. * @return {BoundingRect} */ getArea: function () { var xExtent = this.getAxis('x').getGlobalExtent(); var yExtent = this.getAxis('y').getGlobalExtent(); var x = Math.min(xExtent[0], xExtent[1]); var y = Math.min(yExtent[0], yExtent[1]); var width = Math.max(xExtent[0], xExtent[1]) - x; var height = Math.max(yExtent[0], yExtent[1]) - y; var rect = new BoundingRect(x, y, width, height); return rect; } }; inherits(Cartesian2D, Cartesian); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Extend axis 2d * @constructor module:echarts/coord/cartesian/Axis2D * @extends {module:echarts/coord/cartesian/Axis} * @param {string} dim * @param {*} scale * @param {Array.} coordExtent * @param {string} axisType * @param {string} position */ var Axis2D = function (dim, scale, coordExtent, axisType, position) { Axis.call(this, dim, scale, coordExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = axisType || 'value'; /** * Axis position * - 'top' * - 'bottom' * - 'left' * - 'right' */ this.position = position || 'bottom'; }; Axis2D.prototype = { constructor: Axis2D, /** * Index of axis, can be used as key */ index: 0, /** * Implemented in . * @return {Array.} * If not on zero of other axis, return null/undefined. * If no axes, return an empty array. */ getAxesOnZeroOf: null, /** * Axis model * @param {module:echarts/coord/cartesian/AxisModel} */ model: null, isHorizontal: function () { var position = this.position; return position === 'top' || position === 'bottom'; }, /** * Each item cooresponds to this.getExtent(), which * means globalExtent[0] may greater than globalExtent[1], * unless `asc` is input. * * @param {boolean} [asc] * @return {Array.} */ getGlobalExtent: function (asc) { var ret = this.getExtent(); ret[0] = this.toGlobalCoord(ret[0]); ret[1] = this.toGlobalCoord(ret[1]); asc && ret[0] > ret[1] && ret.reverse(); return ret; }, getOtherAxis: function () { this.grid.getOtherAxis(); }, /** * @override */ pointToData: function (point, clamp) { return this.coordToData(this.toLocalCoord(point[this.dim === 'x' ? 0 : 1]), clamp); }, /** * Transform global coord to local coord, * i.e. var localCoord = axis.toLocalCoord(80); * designate by module:echarts/coord/cartesian/Grid. * @type {Function} */ toLocalCoord: null, /** * Transform global coord to local coord, * i.e. var globalCoord = axis.toLocalCoord(40); * designate by module:echarts/coord/cartesian/Grid. * @type {Function} */ toGlobalCoord: null }; inherits(Axis2D, Axis); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var defaultOption = { show: true, zlevel: 0, z: 0, // Inverse the axis. inverse: false, // Axis name displayed. name: '', // 'start' | 'middle' | 'end' nameLocation: 'end', // By degree. By default auto rotate by nameLocation. nameRotate: null, nameTruncate: { maxWidth: null, ellipsis: '...', placeholder: '.' }, // Use global text style by default. nameTextStyle: {}, // The gap between axisName and axisLine. nameGap: 15, // Default `false` to support tooltip. silent: false, // Default `false` to avoid legacy user event listener fail. triggerEvent: false, tooltip: { show: false }, axisPointer: {}, axisLine: { show: true, onZero: true, onZeroAxisIndex: null, lineStyle: { color: '#333', width: 1, type: 'solid' }, // The arrow at both ends the the axis. symbol: ['none', 'none'], symbolSize: [10, 15] }, axisTick: { show: true, // Whether axisTick is inside the grid or outside the grid. inside: false, // The length of axisTick. length: 5, lineStyle: { width: 1 } }, axisLabel: { show: true, // Whether axisLabel is inside the grid or outside the grid. inside: false, rotate: 0, // true | false | null/undefined (auto) showMinLabel: null, // true | false | null/undefined (auto) showMaxLabel: null, margin: 8, // formatter: null, fontSize: 12 }, splitLine: { show: true, lineStyle: { color: ['#ccc'], width: 1, type: 'solid' } }, splitArea: { show: false, areaStyle: { color: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'] } } }; var axisDefault = {}; axisDefault.categoryAxis = merge({ // The gap at both ends of the axis. For categoryAxis, boolean. boundaryGap: true, // Set false to faster category collection. // Only usefull in the case like: category is // ['2012-01-01', '2012-01-02', ...], where the input // data has been ensured not duplicate and is large data. // null means "auto": // if axis.data provided, do not deduplication, // else do deduplication. deduplication: null, // splitArea: { // show: false // }, splitLine: { show: false }, axisTick: { // If tick is align with label when boundaryGap is true alignWithLabel: false, interval: 'auto' }, axisLabel: { interval: 'auto' } }, defaultOption); axisDefault.valueAxis = merge({ // The gap at both ends of the axis. For value axis, [GAP, GAP], where // `GAP` can be an absolute pixel number (like `35`), or percent (like `'30%'`) boundaryGap: [0, 0], // TODO // min/max: [30, datamin, 60] or [20, datamin] or [datamin, 60] // Min value of the axis. can be: // + a number // + 'dataMin': use the min value in data. // + null/undefined: auto decide min value (consider pretty look and boundaryGap). // min: null, // Max value of the axis. can be: // + a number // + 'dataMax': use the max value in data. // + null/undefined: auto decide max value (consider pretty look and boundaryGap). // max: null, // Readonly prop, specifies start value of the range when using data zoom. // rangeStart: null // Readonly prop, specifies end value of the range when using data zoom. // rangeEnd: null // Optional value can be: // + `false`: always include value 0. // + `true`: the extent do not consider value 0. // scale: false, // AxisTick and axisLabel and splitLine are caculated based on splitNumber. splitNumber: 5, // Interval specifies the span of the ticks is mandatorily. // interval: null // Specify min interval when auto calculate tick interval. // minInterval: null // Specify max interval when auto calculate tick interval. // maxInterval: null minorTick: { // Minor tick, not available for cateogry axis. show: false, // Split number of minor ticks. The value should be in range of (0, 100) splitNumber: 5, // Lenght of minor tick length: 3, // Same inside with axisTick // Line style lineStyle: { // Default to be same with axisTick } }, minorSplitLine: { show: false, lineStyle: { color: '#eee', width: 1 } } }, defaultOption); axisDefault.timeAxis = defaults({ scale: true, min: 'dataMin', max: 'dataMax' }, axisDefault.valueAxis); axisDefault.logAxis = defaults({ scale: true, logBase: 10 }, axisDefault.valueAxis); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // FIXME axisType is fixed ? var AXIS_TYPES = ['value', 'category', 'time', 'log']; /** * Generate sub axis model class * @param {string} axisName 'x' 'y' 'radius' 'angle' 'parallel' * @param {module:echarts/model/Component} BaseAxisModelClass * @param {Function} axisTypeDefaulter * @param {Object} [extraDefaultOption] */ var axisModelCreator = function (axisName, BaseAxisModelClass, axisTypeDefaulter, extraDefaultOption) { each$1(AXIS_TYPES, function (axisType) { BaseAxisModelClass.extend({ /** * @readOnly */ type: axisName + 'Axis.' + axisType, mergeDefaultAndTheme: function (option, ecModel) { var layoutMode = this.layoutMode; var inputPositionParams = layoutMode ? getLayoutParams(option) : {}; var themeModel = ecModel.getTheme(); merge(option, themeModel.get(axisType + 'Axis')); merge(option, this.getDefaultOption()); option.type = axisTypeDefaulter(axisName, option); if (layoutMode) { mergeLayoutParam(option, inputPositionParams, layoutMode); } }, /** * @override */ optionUpdated: function () { var thisOption = this.option; if (thisOption.type === 'category') { this.__ordinalMeta = OrdinalMeta.createByAxisModel(this); } }, /** * Should not be called before all of 'getInitailData' finished. * Because categories are collected during initializing data. */ getCategories: function (rawData) { var option = this.option; // FIXME // warning if called before all of 'getInitailData' finished. if (option.type === 'category') { if (rawData) { return option.data; } return this.__ordinalMeta.categories; } }, getOrdinalMeta: function () { return this.__ordinalMeta; }, defaultOption: mergeAll( [ {}, axisDefault[axisType + 'Axis'], extraDefaultOption ], true ) }); }); ComponentModel.registerSubTypeDefaulter( axisName + 'Axis', curry(axisTypeDefaulter, axisName) ); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var AxisModel = ComponentModel.extend({ type: 'cartesian2dAxis', /** * @type {module:echarts/coord/cartesian/Axis2D} */ axis: null, /** * @override */ init: function () { AxisModel.superApply(this, 'init', arguments); this.resetRange(); }, /** * @override */ mergeOption: function () { AxisModel.superApply(this, 'mergeOption', arguments); this.resetRange(); }, /** * @override */ restoreData: function () { AxisModel.superApply(this, 'restoreData', arguments); this.resetRange(); }, /** * @override * @return {module:echarts/model/Component} */ getCoordSysModel: function () { return this.ecModel.queryComponents({ mainType: 'grid', index: this.option.gridIndex, id: this.option.gridId })[0]; } }); function getAxisType(axisDim, option) { // Default axis with data is category axis return option.type || (option.data ? 'category' : 'value'); } merge(AxisModel.prototype, axisModelCommonMixin); var extraOption = { // gridIndex: 0, // gridId: '', // Offset is for multiple axis on the same position offset: 0 }; axisModelCreator('x', AxisModel, getAxisType, extraOption); axisModelCreator('y', AxisModel, getAxisType, extraOption); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Grid 是在有直角坐标系的时候必须要存在的 // 所以这里也要被 Cartesian2D 依赖 ComponentModel.extend({ type: 'grid', dependencies: ['xAxis', 'yAxis'], layoutMode: 'box', /** * @type {module:echarts/coord/cartesian/Grid} */ coordinateSystem: null, defaultOption: { show: false, zlevel: 0, z: 0, left: '10%', top: 60, right: '10%', bottom: 60, // If grid size contain label containLabel: false, // width: {totalWidth} - left - right, // height: {totalHeight} - top - bottom, backgroundColor: 'rgba(0,0,0,0)', borderWidth: 1, borderColor: '#ccc' } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Grid is a region which contains at most 4 cartesian systems * * TODO Default cartesian */ // Depends on GridModel, AxisModel, which performs preprocess. /** * Check if the axis is used in the specified grid * @inner */ function isAxisUsedInTheGrid(axisModel, gridModel, ecModel) { return axisModel.getCoordSysModel() === gridModel; } function Grid(gridModel, ecModel, api) { /** * @type {Object.} * @private */ this._coordsMap = {}; /** * @type {Array.} * @private */ this._coordsList = []; /** * @type {Object.>} * @private */ this._axesMap = {}; /** * @type {Array.} * @private */ this._axesList = []; this._initCartesian(gridModel, ecModel, api); this.model = gridModel; } var gridProto = Grid.prototype; gridProto.type = 'grid'; gridProto.axisPointerEnabled = true; gridProto.getRect = function () { return this._rect; }; gridProto.update = function (ecModel, api) { var axesMap = this._axesMap; this._updateScale(ecModel, this.model); each$1(axesMap.x, function (xAxis) { niceScaleExtent(xAxis.scale, xAxis.model); }); each$1(axesMap.y, function (yAxis) { niceScaleExtent(yAxis.scale, yAxis.model); }); // Key: axisDim_axisIndex, value: boolean, whether onZero target. var onZeroRecords = {}; each$1(axesMap.x, function (xAxis) { fixAxisOnZero(axesMap, 'y', xAxis, onZeroRecords); }); each$1(axesMap.y, function (yAxis) { fixAxisOnZero(axesMap, 'x', yAxis, onZeroRecords); }); // Resize again if containLabel is enabled // FIXME It may cause getting wrong grid size in data processing stage this.resize(this.model, api); }; function fixAxisOnZero(axesMap, otherAxisDim, axis, onZeroRecords) { axis.getAxesOnZeroOf = function () { // TODO: onZero of multiple axes. return otherAxisOnZeroOf ? [otherAxisOnZeroOf] : []; }; // onZero can not be enabled in these two situations: // 1. When any other axis is a category axis. // 2. When no axis is cross 0 point. var otherAxes = axesMap[otherAxisDim]; var otherAxisOnZeroOf; var axisModel = axis.model; var onZero = axisModel.get('axisLine.onZero'); var onZeroAxisIndex = axisModel.get('axisLine.onZeroAxisIndex'); if (!onZero) { return; } // If target axis is specified. if (onZeroAxisIndex != null) { if (canOnZeroToAxis(otherAxes[onZeroAxisIndex])) { otherAxisOnZeroOf = otherAxes[onZeroAxisIndex]; } } else { // Find the first available other axis. for (var idx in otherAxes) { if (otherAxes.hasOwnProperty(idx) && canOnZeroToAxis(otherAxes[idx]) // Consider that two Y axes on one value axis, // if both onZero, the two Y axes overlap. && !onZeroRecords[getOnZeroRecordKey(otherAxes[idx])] ) { otherAxisOnZeroOf = otherAxes[idx]; break; } } } if (otherAxisOnZeroOf) { onZeroRecords[getOnZeroRecordKey(otherAxisOnZeroOf)] = true; } function getOnZeroRecordKey(axis) { return axis.dim + '_' + axis.index; } } function canOnZeroToAxis(axis) { return axis && axis.type !== 'category' && axis.type !== 'time' && ifAxisCrossZero(axis); } /** * Resize the grid * @param {module:echarts/coord/cartesian/GridModel} gridModel * @param {module:echarts/ExtensionAPI} api */ gridProto.resize = function (gridModel, api, ignoreContainLabel) { var gridRect = getLayoutRect( gridModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() }); this._rect = gridRect; var axesList = this._axesList; adjustAxes(); // Minus label size if (!ignoreContainLabel && gridModel.get('containLabel')) { each$1(axesList, function (axis) { if (!axis.model.get('axisLabel.inside')) { var labelUnionRect = estimateLabelUnionRect(axis); if (labelUnionRect) { var dim = axis.isHorizontal() ? 'height' : 'width'; var margin = axis.model.get('axisLabel.margin'); gridRect[dim] -= labelUnionRect[dim] + margin; if (axis.position === 'top') { gridRect.y += labelUnionRect.height + margin; } else if (axis.position === 'left') { gridRect.x += labelUnionRect.width + margin; } } } }); adjustAxes(); } function adjustAxes() { each$1(axesList, function (axis) { var isHorizontal = axis.isHorizontal(); var extent = isHorizontal ? [0, gridRect.width] : [0, gridRect.height]; var idx = axis.inverse ? 1 : 0; axis.setExtent(extent[idx], extent[1 - idx]); updateAxisTransform(axis, isHorizontal ? gridRect.x : gridRect.y); }); } }; /** * @param {string} axisType * @param {number} [axisIndex] */ gridProto.getAxis = function (axisType, axisIndex) { var axesMapOnDim = this._axesMap[axisType]; if (axesMapOnDim != null) { if (axisIndex == null) { // Find first axis for (var name in axesMapOnDim) { if (axesMapOnDim.hasOwnProperty(name)) { return axesMapOnDim[name]; } } } return axesMapOnDim[axisIndex]; } }; /** * @return {Array.} */ gridProto.getAxes = function () { return this._axesList.slice(); }; /** * Usage: * grid.getCartesian(xAxisIndex, yAxisIndex); * grid.getCartesian(xAxisIndex); * grid.getCartesian(null, yAxisIndex); * grid.getCartesian({xAxisIndex: ..., yAxisIndex: ...}); * * @param {number|Object} [xAxisIndex] * @param {number} [yAxisIndex] */ gridProto.getCartesian = function (xAxisIndex, yAxisIndex) { if (xAxisIndex != null && yAxisIndex != null) { var key = 'x' + xAxisIndex + 'y' + yAxisIndex; return this._coordsMap[key]; } if (isObject$1(xAxisIndex)) { yAxisIndex = xAxisIndex.yAxisIndex; xAxisIndex = xAxisIndex.xAxisIndex; } // When only xAxisIndex or yAxisIndex given, find its first cartesian. for (var i = 0, coordList = this._coordsList; i < coordList.length; i++) { if (coordList[i].getAxis('x').index === xAxisIndex || coordList[i].getAxis('y').index === yAxisIndex ) { return coordList[i]; } } }; gridProto.getCartesians = function () { return this._coordsList.slice(); }; /** * @implements * see {module:echarts/CoodinateSystem} */ gridProto.convertToPixel = function (ecModel, finder, value) { var target = this._findConvertTarget(ecModel, finder); return target.cartesian ? target.cartesian.dataToPoint(value) : target.axis ? target.axis.toGlobalCoord(target.axis.dataToCoord(value)) : null; }; /** * @implements * see {module:echarts/CoodinateSystem} */ gridProto.convertFromPixel = function (ecModel, finder, value) { var target = this._findConvertTarget(ecModel, finder); return target.cartesian ? target.cartesian.pointToData(value) : target.axis ? target.axis.coordToData(target.axis.toLocalCoord(value)) : null; }; /** * @inner */ gridProto._findConvertTarget = function (ecModel, finder) { var seriesModel = finder.seriesModel; var xAxisModel = finder.xAxisModel || (seriesModel && seriesModel.getReferringComponents('xAxis')[0]); var yAxisModel = finder.yAxisModel || (seriesModel && seriesModel.getReferringComponents('yAxis')[0]); var gridModel = finder.gridModel; var coordsList = this._coordsList; var cartesian; var axis; if (seriesModel) { cartesian = seriesModel.coordinateSystem; indexOf(coordsList, cartesian) < 0 && (cartesian = null); } else if (xAxisModel && yAxisModel) { cartesian = this.getCartesian(xAxisModel.componentIndex, yAxisModel.componentIndex); } else if (xAxisModel) { axis = this.getAxis('x', xAxisModel.componentIndex); } else if (yAxisModel) { axis = this.getAxis('y', yAxisModel.componentIndex); } // Lowest priority. else if (gridModel) { var grid = gridModel.coordinateSystem; if (grid === this) { cartesian = this._coordsList[0]; } } return {cartesian: cartesian, axis: axis}; }; /** * @implements * see {module:echarts/CoodinateSystem} */ gridProto.containPoint = function (point) { var coord = this._coordsList[0]; if (coord) { return coord.containPoint(point); } }; /** * Initialize cartesian coordinate systems * @private */ gridProto._initCartesian = function (gridModel, ecModel, api) { var axisPositionUsed = { left: false, right: false, top: false, bottom: false }; var axesMap = { x: {}, y: {} }; var axesCount = { x: 0, y: 0 }; /// Create axis ecModel.eachComponent('xAxis', createAxisCreator('x'), this); ecModel.eachComponent('yAxis', createAxisCreator('y'), this); if (!axesCount.x || !axesCount.y) { // Roll back when there no either x or y axis this._axesMap = {}; this._axesList = []; return; } this._axesMap = axesMap; /// Create cartesian2d each$1(axesMap.x, function (xAxis, xAxisIndex) { each$1(axesMap.y, function (yAxis, yAxisIndex) { var key = 'x' + xAxisIndex + 'y' + yAxisIndex; var cartesian = new Cartesian2D(key); cartesian.grid = this; cartesian.model = gridModel; this._coordsMap[key] = cartesian; this._coordsList.push(cartesian); cartesian.addAxis(xAxis); cartesian.addAxis(yAxis); }, this); }, this); function createAxisCreator(axisType) { return function (axisModel, idx) { if (!isAxisUsedInTheGrid(axisModel, gridModel, ecModel)) { return; } var axisPosition = axisModel.get('position'); if (axisType === 'x') { // Fix position if (axisPosition !== 'top' && axisPosition !== 'bottom') { // Default bottom of X axisPosition = axisPositionUsed.bottom ? 'top' : 'bottom'; } } else { // Fix position if (axisPosition !== 'left' && axisPosition !== 'right') { // Default left of Y axisPosition = axisPositionUsed.left ? 'right' : 'left'; } } axisPositionUsed[axisPosition] = true; var axis = new Axis2D( axisType, createScaleByModel(axisModel), [0, 0], axisModel.get('type'), axisPosition ); var isCategory = axis.type === 'category'; axis.onBand = isCategory && axisModel.get('boundaryGap'); axis.inverse = axisModel.get('inverse'); // Inject axis into axisModel axisModel.axis = axis; // Inject axisModel into axis axis.model = axisModel; // Inject grid info axis axis.grid = this; // Index of axis, can be used as key axis.index = idx; this._axesList.push(axis); axesMap[axisType][idx] = axis; axesCount[axisType]++; }; } }; /** * Update cartesian properties from series * @param {module:echarts/model/Option} option * @private */ gridProto._updateScale = function (ecModel, gridModel) { // Reset scale each$1(this._axesList, function (axis) { axis.scale.setExtent(Infinity, -Infinity); }); ecModel.eachSeries(function (seriesModel) { if (isCartesian2D(seriesModel)) { var axesModels = findAxesModels(seriesModel, ecModel); var xAxisModel = axesModels[0]; var yAxisModel = axesModels[1]; if (!isAxisUsedInTheGrid(xAxisModel, gridModel, ecModel) || !isAxisUsedInTheGrid(yAxisModel, gridModel, ecModel) ) { return; } var cartesian = this.getCartesian( xAxisModel.componentIndex, yAxisModel.componentIndex ); var data = seriesModel.getData(); var xAxis = cartesian.getAxis('x'); var yAxis = cartesian.getAxis('y'); if (data.type === 'list') { unionExtent(data, xAxis, seriesModel); unionExtent(data, yAxis, seriesModel); } } }, this); function unionExtent(data, axis, seriesModel) { each$1(data.mapDimension(axis.dim, true), function (dim) { axis.scale.unionExtentFromData( // For example, the extent of the orginal dimension // is [0.1, 0.5], the extent of the `stackResultDimension` // is [7, 9], the final extent should not include [0.1, 0.5]. data, getStackedDimension(data, dim) ); }); } }; /** * @param {string} [dim] 'x' or 'y' or 'auto' or null/undefined * @return {Object} {baseAxes: [], otherAxes: []} */ gridProto.getTooltipAxes = function (dim) { var baseAxes = []; var otherAxes = []; each$1(this.getCartesians(), function (cartesian) { var baseAxis = (dim != null && dim !== 'auto') ? cartesian.getAxis(dim) : cartesian.getBaseAxis(); var otherAxis = cartesian.getOtherAxis(baseAxis); indexOf(baseAxes, baseAxis) < 0 && baseAxes.push(baseAxis); indexOf(otherAxes, otherAxis) < 0 && otherAxes.push(otherAxis); }); return {baseAxes: baseAxes, otherAxes: otherAxes}; }; /** * @inner */ function updateAxisTransform(axis, coordBase) { var axisExtent = axis.getExtent(); var axisExtentSum = axisExtent[0] + axisExtent[1]; // Fast transform axis.toGlobalCoord = axis.dim === 'x' ? function (coord) { return coord + coordBase; } : function (coord) { return axisExtentSum - coord + coordBase; }; axis.toLocalCoord = axis.dim === 'x' ? function (coord) { return coord - coordBase; } : function (coord) { return axisExtentSum - coord + coordBase; }; } var axesTypes = ['xAxis', 'yAxis']; /** * @inner */ function findAxesModels(seriesModel, ecModel) { return map(axesTypes, function (axisType) { var axisModel = seriesModel.getReferringComponents(axisType)[0]; if (__DEV__) { if (!axisModel) { throw new Error(axisType + ' "' + retrieve( seriesModel.get(axisType + 'Index'), seriesModel.get(axisType + 'Id'), 0 ) + '" not found'); } } return axisModel; }); } /** * @inner */ function isCartesian2D(seriesModel) { return seriesModel.get('coordinateSystem') === 'cartesian2d'; } Grid.create = function (ecModel, api) { var grids = []; ecModel.eachComponent('grid', function (gridModel, idx) { var grid = new Grid(gridModel, ecModel, api); grid.name = 'grid_' + idx; // dataSampling requires axis extent, so resize // should be performed in create stage. grid.resize(gridModel, api, true); gridModel.coordinateSystem = grid; grids.push(grid); }); // Inject the coordinateSystems into seriesModel ecModel.eachSeries(function (seriesModel) { if (!isCartesian2D(seriesModel)) { return; } var axesModels = findAxesModels(seriesModel, ecModel); var xAxisModel = axesModels[0]; var yAxisModel = axesModels[1]; var gridModel = xAxisModel.getCoordSysModel(); if (__DEV__) { if (!gridModel) { throw new Error( 'Grid "' + retrieve( xAxisModel.get('gridIndex'), xAxisModel.get('gridId'), 0 ) + '" not found' ); } if (xAxisModel.getCoordSysModel() !== yAxisModel.getCoordSysModel()) { throw new Error('xAxis and yAxis must use the same grid'); } } var grid = gridModel.coordinateSystem; seriesModel.coordinateSystem = grid.getCartesian( xAxisModel.componentIndex, yAxisModel.componentIndex ); }); return grids; }; // For deciding which dimensions to use when creating list data Grid.dimensions = Grid.prototype.dimensions = Cartesian2D.prototype.dimensions; CoordinateSystemManager.register('cartesian2d', Grid); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var PI$2 = Math.PI; /** * A final axis is translated and rotated from a "standard axis". * So opt.position and opt.rotation is required. * * A standard axis is and axis from [0, 0] to [0, axisExtent[1]], * for example: (0, 0) ------------> (0, 50) * * nameDirection or tickDirection or labelDirection is 1 means tick * or label is below the standard axis, whereas is -1 means above * the standard axis. labelOffset means offset between label and axis, * which is useful when 'onZero', where axisLabel is in the grid and * label in outside grid. * * Tips: like always, * positive rotation represents anticlockwise, and negative rotation * represents clockwise. * The direction of position coordinate is the same as the direction * of screen coordinate. * * Do not need to consider axis 'inverse', which is auto processed by * axis extent. * * @param {module:zrender/container/Group} group * @param {Object} axisModel * @param {Object} opt Standard axis parameters. * @param {Array.} opt.position [x, y] * @param {number} opt.rotation by radian * @param {number} [opt.nameDirection=1] 1 or -1 Used when nameLocation is 'middle' or 'center'. * @param {number} [opt.tickDirection=1] 1 or -1 * @param {number} [opt.labelDirection=1] 1 or -1 * @param {number} [opt.labelOffset=0] Usefull when onZero. * @param {string} [opt.axisLabelShow] default get from axisModel. * @param {string} [opt.axisName] default get from axisModel. * @param {number} [opt.axisNameAvailableWidth] * @param {number} [opt.labelRotate] by degree, default get from axisModel. * @param {number} [opt.strokeContainThreshold] Default label interval when label * @param {number} [opt.nameTruncateMaxWidth] */ var AxisBuilder = function (axisModel, opt) { /** * @readOnly */ this.opt = opt; /** * @readOnly */ this.axisModel = axisModel; // Default value defaults( opt, { labelOffset: 0, nameDirection: 1, tickDirection: 1, labelDirection: 1, silent: true } ); /** * @readOnly */ this.group = new Group(); // FIXME Not use a seperate text group? var dumbGroup = new Group({ position: opt.position.slice(), rotation: opt.rotation }); // this.group.add(dumbGroup); // this._dumbGroup = dumbGroup; dumbGroup.updateTransform(); this._transform = dumbGroup.transform; this._dumbGroup = dumbGroup; }; AxisBuilder.prototype = { constructor: AxisBuilder, hasBuilder: function (name) { return !!builders[name]; }, add: function (name) { builders[name].call(this); }, getGroup: function () { return this.group; } }; var builders = { /** * @private */ axisLine: function () { var opt = this.opt; var axisModel = this.axisModel; if (!axisModel.get('axisLine.show')) { return; } var extent = this.axisModel.axis.getExtent(); var matrix = this._transform; var pt1 = [extent[0], 0]; var pt2 = [extent[1], 0]; if (matrix) { applyTransform(pt1, pt1, matrix); applyTransform(pt2, pt2, matrix); } var lineStyle = extend( { lineCap: 'round' }, axisModel.getModel('axisLine.lineStyle').getLineStyle() ); this.group.add(new Line({ // Id for animation anid: 'line', subPixelOptimize: true, shape: { x1: pt1[0], y1: pt1[1], x2: pt2[0], y2: pt2[1] }, style: lineStyle, strokeContainThreshold: opt.strokeContainThreshold || 5, silent: true, z2: 1 })); var arrows = axisModel.get('axisLine.symbol'); var arrowSize = axisModel.get('axisLine.symbolSize'); var arrowOffset = axisModel.get('axisLine.symbolOffset') || 0; if (typeof arrowOffset === 'number') { arrowOffset = [arrowOffset, arrowOffset]; } if (arrows != null) { if (typeof arrows === 'string') { // Use the same arrow for start and end point arrows = [arrows, arrows]; } if (typeof arrowSize === 'string' || typeof arrowSize === 'number' ) { // Use the same size for width and height arrowSize = [arrowSize, arrowSize]; } var symbolWidth = arrowSize[0]; var symbolHeight = arrowSize[1]; each$1([{ rotate: opt.rotation + Math.PI / 2, offset: arrowOffset[0], r: 0 }, { rotate: opt.rotation - Math.PI / 2, offset: arrowOffset[1], r: Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) }], function (point, index) { if (arrows[index] !== 'none' && arrows[index] != null) { var symbol = createSymbol( arrows[index], -symbolWidth / 2, -symbolHeight / 2, symbolWidth, symbolHeight, lineStyle.stroke, true ); // Calculate arrow position with offset var r = point.r + point.offset; var pos = [ pt1[0] + r * Math.cos(opt.rotation), pt1[1] - r * Math.sin(opt.rotation) ]; symbol.attr({ rotation: point.rotate, position: pos, silent: true, z2: 11 }); this.group.add(symbol); } }, this); } }, /** * @private */ axisTickLabel: function () { var axisModel = this.axisModel; var opt = this.opt; var ticksEls = buildAxisMajorTicks(this, axisModel, opt); var labelEls = buildAxisLabel(this, axisModel, opt); fixMinMaxLabelShow(axisModel, labelEls, ticksEls); buildAxisMinorTicks(this, axisModel, opt); }, /** * @private */ axisName: function () { var opt = this.opt; var axisModel = this.axisModel; var name = retrieve(opt.axisName, axisModel.get('name')); if (!name) { return; } var nameLocation = axisModel.get('nameLocation'); var nameDirection = opt.nameDirection; var textStyleModel = axisModel.getModel('nameTextStyle'); var gap = axisModel.get('nameGap') || 0; var extent = this.axisModel.axis.getExtent(); var gapSignal = extent[0] > extent[1] ? -1 : 1; var pos = [ nameLocation === 'start' ? extent[0] - gapSignal * gap : nameLocation === 'end' ? extent[1] + gapSignal * gap : (extent[0] + extent[1]) / 2, // 'middle' // Reuse labelOffset. isNameLocationCenter(nameLocation) ? opt.labelOffset + nameDirection * gap : 0 ]; var labelLayout; var nameRotation = axisModel.get('nameRotate'); if (nameRotation != null) { nameRotation = nameRotation * PI$2 / 180; // To radian. } var axisNameAvailableWidth; if (isNameLocationCenter(nameLocation)) { labelLayout = innerTextLayout( opt.rotation, nameRotation != null ? nameRotation : opt.rotation, // Adapt to axis. nameDirection ); } else { labelLayout = endTextLayout( opt, nameLocation, nameRotation || 0, extent ); axisNameAvailableWidth = opt.axisNameAvailableWidth; if (axisNameAvailableWidth != null) { axisNameAvailableWidth = Math.abs( axisNameAvailableWidth / Math.sin(labelLayout.rotation) ); !isFinite(axisNameAvailableWidth) && (axisNameAvailableWidth = null); } } var textFont = textStyleModel.getFont(); var truncateOpt = axisModel.get('nameTruncate', true) || {}; var ellipsis = truncateOpt.ellipsis; var maxWidth = retrieve( opt.nameTruncateMaxWidth, truncateOpt.maxWidth, axisNameAvailableWidth ); // FIXME // truncate rich text? (consider performance) var truncatedText = (ellipsis != null && maxWidth != null) ? truncateText$1( name, maxWidth, textFont, ellipsis, {minChar: 2, placeholder: truncateOpt.placeholder} ) : name; var tooltipOpt = axisModel.get('tooltip', true); var mainType = axisModel.mainType; var formatterParams = { componentType: mainType, name: name, $vars: ['name'] }; formatterParams[mainType + 'Index'] = axisModel.componentIndex; var textEl = new Text({ // Id for animation anid: 'name', __fullText: name, __truncatedText: truncatedText, position: pos, rotation: labelLayout.rotation, silent: isLabelSilent(axisModel), z2: 1, tooltip: (tooltipOpt && tooltipOpt.show) ? extend({ content: name, formatter: function () { return name; }, formatterParams: formatterParams }, tooltipOpt) : null }); setTextStyle(textEl.style, textStyleModel, { text: truncatedText, textFont: textFont, textFill: textStyleModel.getTextColor() || axisModel.get('axisLine.lineStyle.color'), textAlign: textStyleModel.get('align') || labelLayout.textAlign, textVerticalAlign: textStyleModel.get('verticalAlign') || labelLayout.textVerticalAlign }); if (axisModel.get('triggerEvent')) { textEl.eventData = makeAxisEventDataBase(axisModel); textEl.eventData.targetType = 'axisName'; textEl.eventData.name = name; } // FIXME this._dumbGroup.add(textEl); textEl.updateTransform(); this.group.add(textEl); textEl.decomposeTransform(); } }; var makeAxisEventDataBase = AxisBuilder.makeAxisEventDataBase = function (axisModel) { var eventData = { componentType: axisModel.mainType, componentIndex: axisModel.componentIndex }; eventData[axisModel.mainType + 'Index'] = axisModel.componentIndex; return eventData; }; /** * @public * @static * @param {Object} opt * @param {number} axisRotation in radian * @param {number} textRotation in radian * @param {number} direction * @return {Object} { * rotation, // according to axis * textAlign, * textVerticalAlign * } */ var innerTextLayout = AxisBuilder.innerTextLayout = function (axisRotation, textRotation, direction) { var rotationDiff = remRadian(textRotation - axisRotation); var textAlign; var textVerticalAlign; if (isRadianAroundZero(rotationDiff)) { // Label is parallel with axis line. textVerticalAlign = direction > 0 ? 'top' : 'bottom'; textAlign = 'center'; } else if (isRadianAroundZero(rotationDiff - PI$2)) { // Label is inverse parallel with axis line. textVerticalAlign = direction > 0 ? 'bottom' : 'top'; textAlign = 'center'; } else { textVerticalAlign = 'middle'; if (rotationDiff > 0 && rotationDiff < PI$2) { textAlign = direction > 0 ? 'right' : 'left'; } else { textAlign = direction > 0 ? 'left' : 'right'; } } return { rotation: rotationDiff, textAlign: textAlign, textVerticalAlign: textVerticalAlign }; }; function endTextLayout(opt, textPosition, textRotate, extent) { var rotationDiff = remRadian(textRotate - opt.rotation); var textAlign; var textVerticalAlign; var inverse = extent[0] > extent[1]; var onLeft = (textPosition === 'start' && !inverse) || (textPosition !== 'start' && inverse); if (isRadianAroundZero(rotationDiff - PI$2 / 2)) { textVerticalAlign = onLeft ? 'bottom' : 'top'; textAlign = 'center'; } else if (isRadianAroundZero(rotationDiff - PI$2 * 1.5)) { textVerticalAlign = onLeft ? 'top' : 'bottom'; textAlign = 'center'; } else { textVerticalAlign = 'middle'; if (rotationDiff < PI$2 * 1.5 && rotationDiff > PI$2 / 2) { textAlign = onLeft ? 'left' : 'right'; } else { textAlign = onLeft ? 'right' : 'left'; } } return { rotation: rotationDiff, textAlign: textAlign, textVerticalAlign: textVerticalAlign }; } var isLabelSilent = AxisBuilder.isLabelSilent = function (axisModel) { var tooltipOpt = axisModel.get('tooltip'); return axisModel.get('silent') // Consider mouse cursor, add these restrictions. || !( axisModel.get('triggerEvent') || (tooltipOpt && tooltipOpt.show) ); }; function fixMinMaxLabelShow(axisModel, labelEls, tickEls) { if (shouldShowAllLabels(axisModel.axis)) { return; } // If min or max are user set, we need to check // If the tick on min(max) are overlap on their neighbour tick // If they are overlapped, we need to hide the min(max) tick label var showMinLabel = axisModel.get('axisLabel.showMinLabel'); var showMaxLabel = axisModel.get('axisLabel.showMaxLabel'); // FIXME // Have not consider onBand yet, where tick els is more than label els. labelEls = labelEls || []; tickEls = tickEls || []; var firstLabel = labelEls[0]; var nextLabel = labelEls[1]; var lastLabel = labelEls[labelEls.length - 1]; var prevLabel = labelEls[labelEls.length - 2]; var firstTick = tickEls[0]; var nextTick = tickEls[1]; var lastTick = tickEls[tickEls.length - 1]; var prevTick = tickEls[tickEls.length - 2]; if (showMinLabel === false) { ignoreEl(firstLabel); ignoreEl(firstTick); } else if (isTwoLabelOverlapped(firstLabel, nextLabel)) { if (showMinLabel) { ignoreEl(nextLabel); ignoreEl(nextTick); } else { ignoreEl(firstLabel); ignoreEl(firstTick); } } if (showMaxLabel === false) { ignoreEl(lastLabel); ignoreEl(lastTick); } else if (isTwoLabelOverlapped(prevLabel, lastLabel)) { if (showMaxLabel) { ignoreEl(prevLabel); ignoreEl(prevTick); } else { ignoreEl(lastLabel); ignoreEl(lastTick); } } } function ignoreEl(el) { el && (el.ignore = true); } function isTwoLabelOverlapped(current, next, labelLayout) { // current and next has the same rotation. var firstRect = current && current.getBoundingRect().clone(); var nextRect = next && next.getBoundingRect().clone(); if (!firstRect || !nextRect) { return; } // When checking intersect of two rotated labels, we use mRotationBack // to avoid that boundingRect is enlarge when using `boundingRect.applyTransform`. var mRotationBack = identity([]); rotate(mRotationBack, mRotationBack, -current.rotation); firstRect.applyTransform(mul$1([], mRotationBack, current.getLocalTransform())); nextRect.applyTransform(mul$1([], mRotationBack, next.getLocalTransform())); return firstRect.intersect(nextRect); } function isNameLocationCenter(nameLocation) { return nameLocation === 'middle' || nameLocation === 'center'; } function createTicks(ticksCoords, tickTransform, tickEndCoord, tickLineStyle, aniid) { var tickEls = []; var pt1 = []; var pt2 = []; for (var i = 0; i < ticksCoords.length; i++) { var tickCoord = ticksCoords[i].coord; pt1[0] = tickCoord; pt1[1] = 0; pt2[0] = tickCoord; pt2[1] = tickEndCoord; if (tickTransform) { applyTransform(pt1, pt1, tickTransform); applyTransform(pt2, pt2, tickTransform); } // Tick line, Not use group transform to have better line draw var tickEl = new Line({ // Id for animation anid: aniid + '_' + ticksCoords[i].tickValue, subPixelOptimize: true, shape: { x1: pt1[0], y1: pt1[1], x2: pt2[0], y2: pt2[1] }, style: tickLineStyle, z2: 2, silent: true }); tickEls.push(tickEl); } return tickEls; } function buildAxisMajorTicks(axisBuilder, axisModel, opt) { var axis = axisModel.axis; var tickModel = axisModel.getModel('axisTick'); if (!tickModel.get('show') || axis.scale.isBlank()) { return; } var lineStyleModel = tickModel.getModel('lineStyle'); var tickEndCoord = opt.tickDirection * tickModel.get('length'); var ticksCoords = axis.getTicksCoords(); var ticksEls = createTicks(ticksCoords, axisBuilder._transform, tickEndCoord, defaults( lineStyleModel.getLineStyle(), { stroke: axisModel.get('axisLine.lineStyle.color') } ), 'ticks'); for (var i = 0; i < ticksEls.length; i++) { axisBuilder.group.add(ticksEls[i]); } return ticksEls; } function buildAxisMinorTicks(axisBuilder, axisModel, opt) { var axis = axisModel.axis; var minorTickModel = axisModel.getModel('minorTick'); if (!minorTickModel.get('show') || axis.scale.isBlank()) { return; } var minorTicksCoords = axis.getMinorTicksCoords(); if (!minorTicksCoords.length) { return; } var lineStyleModel = minorTickModel.getModel('lineStyle'); var tickEndCoord = opt.tickDirection * minorTickModel.get('length'); var minorTickLineStyle = defaults( lineStyleModel.getLineStyle(), defaults( axisModel.getModel('axisTick').getLineStyle(), { stroke: axisModel.get('axisLine.lineStyle.color') } ) ); for (var i = 0; i < minorTicksCoords.length; i++) { var minorTicksEls = createTicks( minorTicksCoords[i], axisBuilder._transform, tickEndCoord, minorTickLineStyle, 'minorticks_' + i ); for (var k = 0; k < minorTicksEls.length; k++) { axisBuilder.group.add(minorTicksEls[k]); } } } function buildAxisLabel(axisBuilder, axisModel, opt) { var axis = axisModel.axis; var show = retrieve(opt.axisLabelShow, axisModel.get('axisLabel.show')); if (!show || axis.scale.isBlank()) { return; } var labelModel = axisModel.getModel('axisLabel'); var labelMargin = labelModel.get('margin'); var labels = axis.getViewLabels(); // Special label rotate. var labelRotation = ( retrieve(opt.labelRotate, labelModel.get('rotate')) || 0 ) * PI$2 / 180; var labelLayout = innerTextLayout(opt.rotation, labelRotation, opt.labelDirection); var rawCategoryData = axisModel.getCategories && axisModel.getCategories(true); var labelEls = []; var silent = isLabelSilent(axisModel); var triggerEvent = axisModel.get('triggerEvent'); each$1(labels, function (labelItem, index) { var tickValue = labelItem.tickValue; var formattedLabel = labelItem.formattedLabel; var rawLabel = labelItem.rawLabel; var itemLabelModel = labelModel; if (rawCategoryData && rawCategoryData[tickValue] && rawCategoryData[tickValue].textStyle) { itemLabelModel = new Model( rawCategoryData[tickValue].textStyle, labelModel, axisModel.ecModel ); } var textColor = itemLabelModel.getTextColor() || axisModel.get('axisLine.lineStyle.color'); var tickCoord = axis.dataToCoord(tickValue); var pos = [ tickCoord, opt.labelOffset + opt.labelDirection * labelMargin ]; var textEl = new Text({ // Id for animation anid: 'label_' + tickValue, position: pos, rotation: labelLayout.rotation, silent: silent, z2: 10 }); setTextStyle(textEl.style, itemLabelModel, { text: formattedLabel, textAlign: itemLabelModel.getShallow('align', true) || labelLayout.textAlign, textVerticalAlign: itemLabelModel.getShallow('verticalAlign', true) || itemLabelModel.getShallow('baseline', true) || labelLayout.textVerticalAlign, textFill: typeof textColor === 'function' ? textColor( // (1) In category axis with data zoom, tick is not the original // index of axis.data. So tick should not be exposed to user // in category axis. // (2) Compatible with previous version, which always use formatted label as // input. But in interval scale the formatted label is like '223,445', which // maked user repalce ','. So we modify it to return original val but remain // it as 'string' to avoid error in replacing. axis.type === 'category' ? rawLabel : axis.type === 'value' ? tickValue + '' : tickValue, index ) : textColor }); // Pack data for mouse event if (triggerEvent) { textEl.eventData = makeAxisEventDataBase(axisModel); textEl.eventData.targetType = 'axisLabel'; textEl.eventData.value = rawLabel; } // FIXME axisBuilder._dumbGroup.add(textEl); textEl.updateTransform(); labelEls.push(textEl); axisBuilder.group.add(textEl); textEl.decomposeTransform(); }); return labelEls; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$6 = each$1; var curry$1 = curry; // Build axisPointerModel, mergin tooltip.axisPointer model for each axis. // allAxesInfo should be updated when setOption performed. function collect(ecModel, api) { var result = { /** * key: makeKey(axis.model) * value: { * axis, * coordSys, * axisPointerModel, * triggerTooltip, * involveSeries, * snap, * seriesModels, * seriesDataCount * } */ axesInfo: {}, seriesInvolved: false, /** * key: makeKey(coordSys.model) * value: Object: key makeKey(axis.model), value: axisInfo */ coordSysAxesInfo: {}, coordSysMap: {} }; collectAxesInfo(result, ecModel, api); // Check seriesInvolved for performance, in case too many series in some chart. result.seriesInvolved && collectSeriesInfo(result, ecModel); return result; } function collectAxesInfo(result, ecModel, api) { var globalTooltipModel = ecModel.getComponent('tooltip'); var globalAxisPointerModel = ecModel.getComponent('axisPointer'); // links can only be set on global. var linksOption = globalAxisPointerModel.get('link', true) || []; var linkGroups = []; // Collect axes info. each$6(api.getCoordinateSystems(), function (coordSys) { // Some coordinate system do not support axes, like geo. if (!coordSys.axisPointerEnabled) { return; } var coordSysKey = makeKey(coordSys.model); var axesInfoInCoordSys = result.coordSysAxesInfo[coordSysKey] = {}; result.coordSysMap[coordSysKey] = coordSys; // Set tooltip (like 'cross') is a convienent way to show axisPointer // for user. So we enable seting tooltip on coordSys model. var coordSysModel = coordSys.model; var baseTooltipModel = coordSysModel.getModel('tooltip', globalTooltipModel); each$6(coordSys.getAxes(), curry$1(saveTooltipAxisInfo, false, null)); // If axis tooltip used, choose tooltip axis for each coordSys. // Notice this case: coordSys is `grid` but not `cartesian2D` here. if (coordSys.getTooltipAxes && globalTooltipModel // If tooltip.showContent is set as false, tooltip will not // show but axisPointer will show as normal. && baseTooltipModel.get('show') ) { // Compatible with previous logic. But series.tooltip.trigger: 'axis' // or series.data[n].tooltip.trigger: 'axis' are not support any more. var triggerAxis = baseTooltipModel.get('trigger') === 'axis'; var cross = baseTooltipModel.get('axisPointer.type') === 'cross'; var tooltipAxes = coordSys.getTooltipAxes(baseTooltipModel.get('axisPointer.axis')); if (triggerAxis || cross) { each$6(tooltipAxes.baseAxes, curry$1( saveTooltipAxisInfo, cross ? 'cross' : true, triggerAxis )); } if (cross) { each$6(tooltipAxes.otherAxes, curry$1(saveTooltipAxisInfo, 'cross', false)); } } // fromTooltip: true | false | 'cross' // triggerTooltip: true | false | null function saveTooltipAxisInfo(fromTooltip, triggerTooltip, axis) { var axisPointerModel = axis.model.getModel('axisPointer', globalAxisPointerModel); var axisPointerShow = axisPointerModel.get('show'); if (!axisPointerShow || ( axisPointerShow === 'auto' && !fromTooltip && !isHandleTrigger(axisPointerModel) )) { return; } if (triggerTooltip == null) { triggerTooltip = axisPointerModel.get('triggerTooltip'); } axisPointerModel = fromTooltip ? makeAxisPointerModel( axis, baseTooltipModel, globalAxisPointerModel, ecModel, fromTooltip, triggerTooltip ) : axisPointerModel; var snap = axisPointerModel.get('snap'); var key = makeKey(axis.model); var involveSeries = triggerTooltip || snap || axis.type === 'category'; // If result.axesInfo[key] exist, override it (tooltip has higher priority). var axisInfo = result.axesInfo[key] = { key: key, axis: axis, coordSys: coordSys, axisPointerModel: axisPointerModel, triggerTooltip: triggerTooltip, involveSeries: involveSeries, snap: snap, useHandle: isHandleTrigger(axisPointerModel), seriesModels: [] }; axesInfoInCoordSys[key] = axisInfo; result.seriesInvolved |= involveSeries; var groupIndex = getLinkGroupIndex(linksOption, axis); if (groupIndex != null) { var linkGroup = linkGroups[groupIndex] || (linkGroups[groupIndex] = {axesInfo: {}}); linkGroup.axesInfo[key] = axisInfo; linkGroup.mapper = linksOption[groupIndex].mapper; axisInfo.linkGroup = linkGroup; } } }); } function makeAxisPointerModel( axis, baseTooltipModel, globalAxisPointerModel, ecModel, fromTooltip, triggerTooltip ) { var tooltipAxisPointerModel = baseTooltipModel.getModel('axisPointer'); var volatileOption = {}; each$6( [ 'type', 'snap', 'lineStyle', 'shadowStyle', 'label', 'animation', 'animationDurationUpdate', 'animationEasingUpdate', 'z' ], function (field) { volatileOption[field] = clone(tooltipAxisPointerModel.get(field)); } ); // category axis do not auto snap, otherwise some tick that do not // has value can not be hovered. value/time/log axis default snap if // triggered from tooltip and trigger tooltip. volatileOption.snap = axis.type !== 'category' && !!triggerTooltip; // Compatibel with previous behavior, tooltip axis do not show label by default. // Only these properties can be overrided from tooltip to axisPointer. if (tooltipAxisPointerModel.get('type') === 'cross') { volatileOption.type = 'line'; } var labelOption = volatileOption.label || (volatileOption.label = {}); // Follow the convention, do not show label when triggered by tooltip by default. labelOption.show == null && (labelOption.show = false); if (fromTooltip === 'cross') { // When 'cross', both axes show labels. var tooltipAxisPointerLabelShow = tooltipAxisPointerModel.get('label.show'); labelOption.show = tooltipAxisPointerLabelShow != null ? tooltipAxisPointerLabelShow : true; // If triggerTooltip, this is a base axis, which should better not use cross style // (cross style is dashed by default) if (!triggerTooltip) { var crossStyle = volatileOption.lineStyle = tooltipAxisPointerModel.get('crossStyle'); crossStyle && defaults(labelOption, crossStyle.textStyle); } } return axis.model.getModel( 'axisPointer', new Model(volatileOption, globalAxisPointerModel, ecModel) ); } function collectSeriesInfo(result, ecModel) { // Prepare data for axis trigger ecModel.eachSeries(function (seriesModel) { // Notice this case: this coordSys is `cartesian2D` but not `grid`. var coordSys = seriesModel.coordinateSystem; var seriesTooltipTrigger = seriesModel.get('tooltip.trigger', true); var seriesTooltipShow = seriesModel.get('tooltip.show', true); if (!coordSys || seriesTooltipTrigger === 'none' || seriesTooltipTrigger === false || seriesTooltipTrigger === 'item' || seriesTooltipShow === false || seriesModel.get('axisPointer.show', true) === false ) { return; } each$6(result.coordSysAxesInfo[makeKey(coordSys.model)], function (axisInfo) { var axis = axisInfo.axis; if (coordSys.getAxis(axis.dim) === axis) { axisInfo.seriesModels.push(seriesModel); axisInfo.seriesDataCount == null && (axisInfo.seriesDataCount = 0); axisInfo.seriesDataCount += seriesModel.getData().count(); } }); }, this); } /** * For example: * { * axisPointer: { * links: [{ * xAxisIndex: [2, 4], * yAxisIndex: 'all' * }, { * xAxisId: ['a5', 'a7'], * xAxisName: 'xxx' * }] * } * } */ function getLinkGroupIndex(linksOption, axis) { var axisModel = axis.model; var dim = axis.dim; for (var i = 0; i < linksOption.length; i++) { var linkOption = linksOption[i] || {}; if (checkPropInLink(linkOption[dim + 'AxisId'], axisModel.id) || checkPropInLink(linkOption[dim + 'AxisIndex'], axisModel.componentIndex) || checkPropInLink(linkOption[dim + 'AxisName'], axisModel.name) ) { return i; } } } function checkPropInLink(linkPropValue, axisPropValue) { return linkPropValue === 'all' || (isArray(linkPropValue) && indexOf(linkPropValue, axisPropValue) >= 0) || linkPropValue === axisPropValue; } function fixValue(axisModel) { var axisInfo = getAxisInfo(axisModel); if (!axisInfo) { return; } var axisPointerModel = axisInfo.axisPointerModel; var scale = axisInfo.axis.scale; var option = axisPointerModel.option; var status = axisPointerModel.get('status'); var value = axisPointerModel.get('value'); // Parse init value for category and time axis. if (value != null) { value = scale.parse(value); } var useHandle = isHandleTrigger(axisPointerModel); // If `handle` used, `axisPointer` will always be displayed, so value // and status should be initialized. if (status == null) { option.status = useHandle ? 'show' : 'hide'; } var extent = scale.getExtent().slice(); extent[0] > extent[1] && extent.reverse(); if (// Pick a value on axis when initializing. value == null // If both `handle` and `dataZoom` are used, value may be out of axis extent, // where we should re-pick a value to keep `handle` displaying normally. || value > extent[1] ) { // Make handle displayed on the end of the axis when init, which looks better. value = extent[1]; } if (value < extent[0]) { value = extent[0]; } option.value = value; if (useHandle) { option.status = axisInfo.axis.scale.isBlank() ? 'hide' : 'show'; } } function getAxisInfo(axisModel) { var coordSysAxesInfo = (axisModel.ecModel.getComponent('axisPointer') || {}).coordSysAxesInfo; return coordSysAxesInfo && coordSysAxesInfo.axesInfo[makeKey(axisModel)]; } function getAxisPointerModel(axisModel) { var axisInfo = getAxisInfo(axisModel); return axisInfo && axisInfo.axisPointerModel; } function isHandleTrigger(axisPointerModel) { return !!axisPointerModel.get('handle.show'); } /** * @param {module:echarts/model/Model} model * @return {string} unique key */ function makeKey(model) { return model.type + '||' + model.id; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Base class of AxisView. */ var AxisView = extendComponentView({ type: 'axis', /** * @private */ _axisPointer: null, /** * @protected * @type {string} */ axisPointerClass: null, /** * @override */ render: function (axisModel, ecModel, api, payload) { // FIXME // This process should proformed after coordinate systems updated // (axis scale updated), and should be performed each time update. // So put it here temporarily, although it is not appropriate to // put a model-writing procedure in `view`. this.axisPointerClass && fixValue(axisModel); AxisView.superApply(this, 'render', arguments); updateAxisPointer(this, axisModel, ecModel, api, payload, true); }, /** * Action handler. * @public * @param {module:echarts/coord/cartesian/AxisModel} axisModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @param {Object} payload */ updateAxisPointer: function (axisModel, ecModel, api, payload, force) { updateAxisPointer(this, axisModel, ecModel, api, payload, false); }, /** * @override */ remove: function (ecModel, api) { var axisPointer = this._axisPointer; axisPointer && axisPointer.remove(api); AxisView.superApply(this, 'remove', arguments); }, /** * @override */ dispose: function (ecModel, api) { disposeAxisPointer(this, api); AxisView.superApply(this, 'dispose', arguments); } }); function updateAxisPointer(axisView, axisModel, ecModel, api, payload, forceRender) { var Clazz = AxisView.getAxisPointerClass(axisView.axisPointerClass); if (!Clazz) { return; } var axisPointerModel = getAxisPointerModel(axisModel); axisPointerModel ? (axisView._axisPointer || (axisView._axisPointer = new Clazz())) .render(axisModel, axisPointerModel, api, forceRender) : disposeAxisPointer(axisView, api); } function disposeAxisPointer(axisView, ecModel, api) { var axisPointer = axisView._axisPointer; axisPointer && axisPointer.dispose(ecModel, api); axisView._axisPointer = null; } var axisPointerClazz = []; AxisView.registerAxisPointerClass = function (type, clazz) { if (__DEV__) { if (axisPointerClazz[type]) { throw new Error('axisPointer ' + type + ' exists'); } } axisPointerClazz[type] = clazz; }; AxisView.getAxisPointerClass = function (type) { return type && axisPointerClazz[type]; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Can only be called after coordinate system creation stage. * (Can be called before coordinate system update stage). * * @param {Object} opt {labelInside} * @return {Object} { * position, rotation, labelDirection, labelOffset, * tickDirection, labelRotate, z2 * } */ function layout$1(gridModel, axisModel, opt) { opt = opt || {}; var grid = gridModel.coordinateSystem; var axis = axisModel.axis; var layout = {}; var otherAxisOnZeroOf = axis.getAxesOnZeroOf()[0]; var rawAxisPosition = axis.position; var axisPosition = otherAxisOnZeroOf ? 'onZero' : rawAxisPosition; var axisDim = axis.dim; var rect = grid.getRect(); var rectBound = [rect.x, rect.x + rect.width, rect.y, rect.y + rect.height]; var idx = {left: 0, right: 1, top: 0, bottom: 1, onZero: 2}; var axisOffset = axisModel.get('offset') || 0; var posBound = axisDim === 'x' ? [rectBound[2] - axisOffset, rectBound[3] + axisOffset] : [rectBound[0] - axisOffset, rectBound[1] + axisOffset]; if (otherAxisOnZeroOf) { var onZeroCoord = otherAxisOnZeroOf.toGlobalCoord(otherAxisOnZeroOf.dataToCoord(0)); posBound[idx.onZero] = Math.max(Math.min(onZeroCoord, posBound[1]), posBound[0]); } // Axis position layout.position = [ axisDim === 'y' ? posBound[idx[axisPosition]] : rectBound[0], axisDim === 'x' ? posBound[idx[axisPosition]] : rectBound[3] ]; // Axis rotation layout.rotation = Math.PI / 2 * (axisDim === 'x' ? 0 : 1); // Tick and label direction, x y is axisDim var dirMap = {top: -1, bottom: 1, left: -1, right: 1}; layout.labelDirection = layout.tickDirection = layout.nameDirection = dirMap[rawAxisPosition]; layout.labelOffset = otherAxisOnZeroOf ? posBound[idx[rawAxisPosition]] - posBound[idx.onZero] : 0; if (axisModel.get('axisTick.inside')) { layout.tickDirection = -layout.tickDirection; } if (retrieve(opt.labelInside, axisModel.get('axisLabel.inside'))) { layout.labelDirection = -layout.labelDirection; } // Special label rotation var labelRotate = axisModel.get('axisLabel.rotate'); layout.labelRotate = axisPosition === 'top' ? -labelRotate : labelRotate; // Over splitLine and splitArea layout.z2 = 1; return layout; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function rectCoordAxisBuildSplitArea(axisView, axisGroup, axisModel, gridModel) { var axis = axisModel.axis; if (axis.scale.isBlank()) { return; } var splitAreaModel = axisModel.getModel('splitArea'); var areaStyleModel = splitAreaModel.getModel('areaStyle'); var areaColors = areaStyleModel.get('color'); var gridRect = gridModel.coordinateSystem.getRect(); var ticksCoords = axis.getTicksCoords({ tickModel: splitAreaModel, clamp: true }); if (!ticksCoords.length) { return; } // For Making appropriate splitArea animation, the color and anid // should be corresponding to previous one if possible. var areaColorsLen = areaColors.length; var lastSplitAreaColors = axisView.__splitAreaColors; var newSplitAreaColors = createHashMap(); var colorIndex = 0; if (lastSplitAreaColors) { for (var i = 0; i < ticksCoords.length; i++) { var cIndex = lastSplitAreaColors.get(ticksCoords[i].tickValue); if (cIndex != null) { colorIndex = (cIndex + (areaColorsLen - 1) * i) % areaColorsLen; break; } } } var prev = axis.toGlobalCoord(ticksCoords[0].coord); var areaStyle = areaStyleModel.getAreaStyle(); areaColors = isArray(areaColors) ? areaColors : [areaColors]; for (var i = 1; i < ticksCoords.length; i++) { var tickCoord = axis.toGlobalCoord(ticksCoords[i].coord); var x; var y; var width; var height; if (axis.isHorizontal()) { x = prev; y = gridRect.y; width = tickCoord - x; height = gridRect.height; prev = x + width; } else { x = gridRect.x; y = prev; width = gridRect.width; height = tickCoord - y; prev = y + height; } var tickValue = ticksCoords[i - 1].tickValue; tickValue != null && newSplitAreaColors.set(tickValue, colorIndex); axisGroup.add(new Rect({ anid: tickValue != null ? 'area_' + tickValue : null, shape: { x: x, y: y, width: width, height: height }, style: defaults({ fill: areaColors[colorIndex] }, areaStyle), silent: true })); colorIndex = (colorIndex + 1) % areaColorsLen; } axisView.__splitAreaColors = newSplitAreaColors; } function rectCoordAxisHandleRemove(axisView) { axisView.__splitAreaColors = null; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var axisBuilderAttrs = [ 'axisLine', 'axisTickLabel', 'axisName' ]; var selfBuilderAttrs = [ 'splitArea', 'splitLine', 'minorSplitLine' ]; var CartesianAxisView = AxisView.extend({ type: 'cartesianAxis', axisPointerClass: 'CartesianAxisPointer', /** * @override */ render: function (axisModel, ecModel, api, payload) { this.group.removeAll(); var oldAxisGroup = this._axisGroup; this._axisGroup = new Group(); this.group.add(this._axisGroup); if (!axisModel.get('show')) { return; } var gridModel = axisModel.getCoordSysModel(); var layout = layout$1(gridModel, axisModel); var axisBuilder = new AxisBuilder(axisModel, layout); each$1(axisBuilderAttrs, axisBuilder.add, axisBuilder); this._axisGroup.add(axisBuilder.getGroup()); each$1(selfBuilderAttrs, function (name) { if (axisModel.get(name + '.show')) { this['_' + name](axisModel, gridModel); } }, this); groupTransition(oldAxisGroup, this._axisGroup, axisModel); CartesianAxisView.superCall(this, 'render', axisModel, ecModel, api, payload); }, remove: function () { rectCoordAxisHandleRemove(this); }, /** * @param {module:echarts/coord/cartesian/AxisModel} axisModel * @param {module:echarts/coord/cartesian/GridModel} gridModel * @private */ _splitLine: function (axisModel, gridModel) { var axis = axisModel.axis; if (axis.scale.isBlank()) { return; } var splitLineModel = axisModel.getModel('splitLine'); var lineStyleModel = splitLineModel.getModel('lineStyle'); var lineColors = lineStyleModel.get('color'); lineColors = isArray(lineColors) ? lineColors : [lineColors]; var gridRect = gridModel.coordinateSystem.getRect(); var isHorizontal = axis.isHorizontal(); var lineCount = 0; var ticksCoords = axis.getTicksCoords({ tickModel: splitLineModel }); var p1 = []; var p2 = []; var lineStyle = lineStyleModel.getLineStyle(); for (var i = 0; i < ticksCoords.length; i++) { var tickCoord = axis.toGlobalCoord(ticksCoords[i].coord); if (isHorizontal) { p1[0] = tickCoord; p1[1] = gridRect.y; p2[0] = tickCoord; p2[1] = gridRect.y + gridRect.height; } else { p1[0] = gridRect.x; p1[1] = tickCoord; p2[0] = gridRect.x + gridRect.width; p2[1] = tickCoord; } var colorIndex = (lineCount++) % lineColors.length; var tickValue = ticksCoords[i].tickValue; this._axisGroup.add(new Line({ anid: tickValue != null ? 'line_' + ticksCoords[i].tickValue : null, subPixelOptimize: true, shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] }, style: defaults({ stroke: lineColors[colorIndex] }, lineStyle), silent: true })); } }, /** * @param {module:echarts/coord/cartesian/AxisModel} axisModel * @param {module:echarts/coord/cartesian/GridModel} gridModel * @private */ _minorSplitLine: function (axisModel, gridModel) { var axis = axisModel.axis; var minorSplitLineModel = axisModel.getModel('minorSplitLine'); var lineStyleModel = minorSplitLineModel.getModel('lineStyle'); var gridRect = gridModel.coordinateSystem.getRect(); var isHorizontal = axis.isHorizontal(); var minorTicksCoords = axis.getMinorTicksCoords(); if (!minorTicksCoords.length) { return; } var p1 = []; var p2 = []; var lineStyle = lineStyleModel.getLineStyle(); for (var i = 0; i < minorTicksCoords.length; i++) { for (var k = 0; k < minorTicksCoords[i].length; k++) { var tickCoord = axis.toGlobalCoord(minorTicksCoords[i][k].coord); if (isHorizontal) { p1[0] = tickCoord; p1[1] = gridRect.y; p2[0] = tickCoord; p2[1] = gridRect.y + gridRect.height; } else { p1[0] = gridRect.x; p1[1] = tickCoord; p2[0] = gridRect.x + gridRect.width; p2[1] = tickCoord; } this._axisGroup.add(new Line({ anid: 'minor_line_' + minorTicksCoords[i][k].tickValue, subPixelOptimize: true, shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] }, style: lineStyle, silent: true })); } } }, /** * @param {module:echarts/coord/cartesian/AxisModel} axisModel * @param {module:echarts/coord/cartesian/GridModel} gridModel * @private */ _splitArea: function (axisModel, gridModel) { rectCoordAxisBuildSplitArea(this, this._axisGroup, axisModel, gridModel); } }); CartesianAxisView.extend({ type: 'xAxis' }); CartesianAxisView.extend({ type: 'yAxis' }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Grid view extendComponentView({ type: 'grid', render: function (gridModel, ecModel) { this.group.removeAll(); if (gridModel.get('show')) { this.group.add(new Rect({ shape: gridModel.coordinateSystem.getRect(), style: defaults({ fill: gridModel.get('backgroundColor') }, gridModel.getItemStyle()), silent: true, z2: -1 })); } } }); registerPreprocessor(function (option) { // Only create grid when need if (option.xAxis && option.yAxis && !option.grid) { option.grid = {}; } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // In case developer forget to include grid component registerVisual(visualSymbol('line', 'circle', 'line')); registerLayout(pointsLayout('line')); // Down sample after filter registerProcessor( PRIORITY.PROCESSOR.STATISTIC, dataSample('line') ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var BaseBarSeries = SeriesModel.extend({ type: 'series.__base_bar__', getInitialData: function (option, ecModel) { return createListFromArray(this.getSource(), this, {useEncodeDefaulter: true}); }, getMarkerPosition: function (value) { var coordSys = this.coordinateSystem; if (coordSys) { // PENDING if clamp ? var pt = coordSys.dataToPoint(coordSys.clampData(value)); var data = this.getData(); var offset = data.getLayout('offset'); var size = data.getLayout('size'); var offsetIndex = coordSys.getBaseAxis().isHorizontal() ? 0 : 1; pt[offsetIndex] += offset + size / 2; return pt; } return [NaN, NaN]; }, defaultOption: { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 coordinateSystem: 'cartesian2d', legendHoverLink: true, // stack: null // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, // 最小高度改为0 barMinHeight: 0, // 最小角度为0,仅对极坐标系下的柱状图有效 barMinAngle: 0, // cursor: null, large: false, largeThreshold: 400, progressive: 3e3, progressiveChunkMode: 'mod', // barMaxWidth: null, // In cartesian, the default value is 1. Otherwise null. // barMinWidth: null, // 默认自适应 // barWidth: null, // 柱间距离,默认为柱形宽度的30%,可设固定值 // barGap: '30%', // 类目间柱形距离,默认为类目间距的20%,可设固定值 // barCategoryGap: '20%', // label: { // show: false // }, itemStyle: {}, emphasis: {} } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ BaseBarSeries.extend({ type: 'series.bar', dependencies: ['grid', 'polar'], brushSelector: 'rect', /** * @override */ getProgressive: function () { // Do not support progressive in normal mode. return this.get('large') ? this.get('progressive') : false; }, /** * @override */ getProgressiveThreshold: function () { // Do not support progressive in normal mode. var progressiveThreshold = this.get('progressiveThreshold'); var largeThreshold = this.get('largeThreshold'); if (largeThreshold > progressiveThreshold) { progressiveThreshold = largeThreshold; } return progressiveThreshold; }, defaultOption: { // If clipped // Only available on cartesian2d clip: true, // If use caps on two sides of bars // Only available on tangential polar bar roundCap: false, showBackground: false, backgroundStyle: { color: 'rgba(180, 180, 180, 0.2)', borderColor: null, borderWidth: 0, borderType: 'solid', borderRadius: 0, shadowBlur: 0, shadowColor: null, shadowOffsetX: 0, shadowOffsetY: 0, opacity: 1 } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function setLabel( normalStyle, hoverStyle, itemModel, color, seriesModel, dataIndex, labelPositionOutside ) { var labelModel = itemModel.getModel('label'); var hoverLabelModel = itemModel.getModel('emphasis.label'); setLabelStyle( normalStyle, hoverStyle, labelModel, hoverLabelModel, { labelFetcher: seriesModel, labelDataIndex: dataIndex, defaultText: getDefaultLabel(seriesModel.getData(), dataIndex), isRectText: true, autoColor: color } ); fixPosition(normalStyle); fixPosition(hoverStyle); } function fixPosition(style, labelPositionOutside) { if (style.textPosition === 'outside') { style.textPosition = labelPositionOutside; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var getBarItemStyle = makeStyleMapper( [ ['fill', 'color'], ['stroke', 'borderColor'], ['lineWidth', 'borderWidth'], // Compatitable with 2 ['stroke', 'barBorderColor'], ['lineWidth', 'barBorderWidth'], ['opacity'], ['shadowBlur'], ['shadowOffsetX'], ['shadowOffsetY'], ['shadowColor'] ] ); var barItemStyle = { getBarItemStyle: function (excludes) { var style = getBarItemStyle(this, excludes); if (this.getBorderLineDash) { var lineDash = this.getBorderLineDash(); lineDash && (style.lineDash = lineDash); } return style; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Sausage: similar to sector, but have half circle on both sides * @public */ var Sausage = extendShape({ type: 'sausage', shape: { cx: 0, cy: 0, r0: 0, r: 0, startAngle: 0, endAngle: Math.PI * 2, clockwise: true }, buildPath: function (ctx, shape) { var x = shape.cx; var y = shape.cy; var r0 = Math.max(shape.r0 || 0, 0); var r = Math.max(shape.r, 0); var dr = (r - r0) * 0.5; var rCenter = r0 + dr; var startAngle = shape.startAngle; var endAngle = shape.endAngle; var clockwise = shape.clockwise; var unitStartX = Math.cos(startAngle); var unitStartY = Math.sin(startAngle); var unitEndX = Math.cos(endAngle); var unitEndY = Math.sin(endAngle); var lessThanCircle = clockwise ? endAngle - startAngle < Math.PI * 2 : startAngle - endAngle < Math.PI * 2; if (lessThanCircle) { ctx.moveTo(unitStartX * r0 + x, unitStartY * r0 + y); ctx.arc( unitStartX * rCenter + x, unitStartY * rCenter + y, dr, -Math.PI + startAngle, startAngle, !clockwise ); } ctx.arc(x, y, r, startAngle, endAngle, !clockwise); ctx.moveTo(unitEndX * r + x, unitEndY * r + y); ctx.arc( unitEndX * rCenter + x, unitEndY * rCenter + y, dr, endAngle - Math.PI * 2, endAngle - Math.PI, !clockwise ); if (r0 !== 0) { ctx.arc(x, y, r0, endAngle, startAngle, clockwise); ctx.moveTo(unitStartX * r0 + x, unitEndY * r0 + y); } ctx.closePath(); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'barBorderWidth']; var _eventPos = [0, 0]; // FIXME // Just for compatible with ec2. extend(Model.prototype, barItemStyle); function getClipArea(coord, data) { var coordSysClipArea = coord.getArea && coord.getArea(); if (coord.type === 'cartesian2d') { var baseAxis = coord.getBaseAxis(); // When boundaryGap is false or using time axis. bar may exceed the grid. // We should not clip this part. // See test/bar2.html if (baseAxis.type !== 'category' || !baseAxis.onBand) { var expandWidth = data.getLayout('bandWidth'); if (baseAxis.isHorizontal()) { coordSysClipArea.x -= expandWidth; coordSysClipArea.width += expandWidth * 2; } else { coordSysClipArea.y -= expandWidth; coordSysClipArea.height += expandWidth * 2; } } } return coordSysClipArea; } extendChartView({ type: 'bar', render: function (seriesModel, ecModel, api) { this._updateDrawMode(seriesModel); var coordinateSystemType = seriesModel.get('coordinateSystem'); if (coordinateSystemType === 'cartesian2d' || coordinateSystemType === 'polar' ) { this._isLargeDraw ? this._renderLarge(seriesModel, ecModel, api) : this._renderNormal(seriesModel, ecModel, api); } else if (__DEV__) { console.warn('Only cartesian2d and polar supported for bar.'); } return this.group; }, incrementalPrepareRender: function (seriesModel, ecModel, api) { this._clear(); this._updateDrawMode(seriesModel); }, incrementalRender: function (params, seriesModel, ecModel, api) { // Do not support progressive in normal mode. this._incrementalRenderLarge(params, seriesModel); }, _updateDrawMode: function (seriesModel) { var isLargeDraw = seriesModel.pipelineContext.large; if (this._isLargeDraw == null || isLargeDraw ^ this._isLargeDraw) { this._isLargeDraw = isLargeDraw; this._clear(); } }, _renderNormal: function (seriesModel, ecModel, api) { var group = this.group; var data = seriesModel.getData(); var oldData = this._data; var coord = seriesModel.coordinateSystem; var baseAxis = coord.getBaseAxis(); var isHorizontalOrRadial; if (coord.type === 'cartesian2d') { isHorizontalOrRadial = baseAxis.isHorizontal(); } else if (coord.type === 'polar') { isHorizontalOrRadial = baseAxis.dim === 'angle'; } var animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null; var needsClip = seriesModel.get('clip', true); var coordSysClipArea = getClipArea(coord, data); // If there is clipPath created in large mode. Remove it. group.removeClipPath(); // We don't use clipPath in normal mode because we needs a perfect animation // And don't want the label are clipped. var roundCap = seriesModel.get('roundCap', true); var drawBackground = seriesModel.get('showBackground', true); var backgroundModel = seriesModel.getModel('backgroundStyle'); var barBorderRadius = backgroundModel.get('barBorderRadius') || 0; var bgEls = []; var oldBgEls = this._backgroundEls || []; var createBackground = function (dataIndex) { var bgLayout = getLayout[coord.type](data, dataIndex); var bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout); bgEl.useStyle(backgroundModel.getBarItemStyle()); // Only cartesian2d support borderRadius. if (coord.type === 'cartesian2d') { bgEl.setShape('r', barBorderRadius); } bgEls[dataIndex] = bgEl; return bgEl; }; data.diff(oldData) .add(function (dataIndex) { var itemModel = data.getItemModel(dataIndex); var layout = getLayout[coord.type](data, dataIndex, itemModel); if (drawBackground) { createBackground(dataIndex); } // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy". if (!data.hasValue(dataIndex)) { return; } if (needsClip) { // Clip will modify the layout params. // And return a boolean to determine if the shape are fully clipped. var isClipped = clip[coord.type](coordSysClipArea, layout); if (isClipped) { group.remove(el); return; } } var el = elementCreator[coord.type]( dataIndex, layout, isHorizontalOrRadial, animationModel, false, roundCap ); data.setItemGraphicEl(dataIndex, el); group.add(el); updateStyle( el, data, dataIndex, itemModel, layout, seriesModel, isHorizontalOrRadial, coord.type === 'polar' ); }) .update(function (newIndex, oldIndex) { var itemModel = data.getItemModel(newIndex); var layout = getLayout[coord.type](data, newIndex, itemModel); if (drawBackground) { var bgEl; if (oldBgEls.length === 0) { bgEl = createBackground(oldIndex); } else { bgEl = oldBgEls[oldIndex]; bgEl.useStyle(backgroundModel.getBarItemStyle()); // Only cartesian2d support borderRadius. if (coord.type === 'cartesian2d') { bgEl.setShape('r', barBorderRadius); } bgEls[newIndex] = bgEl; } var bgLayout = getLayout[coord.type](data, newIndex); var shape = createBackgroundShape(isHorizontalOrRadial, bgLayout, coord); updateProps(bgEl, { shape: shape }, animationModel, newIndex); } var el = oldData.getItemGraphicEl(oldIndex); if (!data.hasValue(newIndex)) { group.remove(el); return; } if (needsClip) { var isClipped = clip[coord.type](coordSysClipArea, layout); if (isClipped) { group.remove(el); return; } } if (el) { updateProps(el, {shape: layout}, animationModel, newIndex); } else { el = elementCreator[coord.type]( newIndex, layout, isHorizontalOrRadial, animationModel, true, roundCap ); } data.setItemGraphicEl(newIndex, el); // Add back group.add(el); updateStyle( el, data, newIndex, itemModel, layout, seriesModel, isHorizontalOrRadial, coord.type === 'polar' ); }) .remove(function (dataIndex) { var el = oldData.getItemGraphicEl(dataIndex); if (coord.type === 'cartesian2d') { el && removeRect(dataIndex, animationModel, el); } else { el && removeSector(dataIndex, animationModel, el); } }) .execute(); var bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group()); bgGroup.removeAll(); for (var i = 0; i < bgEls.length; ++i) { bgGroup.add(bgEls[i]); } group.add(bgGroup); this._backgroundEls = bgEls; this._data = data; }, _renderLarge: function (seriesModel, ecModel, api) { this._clear(); createLarge(seriesModel, this.group); // Use clipPath in large mode. var clipPath = seriesModel.get('clip', true) ? createClipPath(seriesModel.coordinateSystem, false, seriesModel) : null; if (clipPath) { this.group.setClipPath(clipPath); } else { this.group.removeClipPath(); } }, _incrementalRenderLarge: function (params, seriesModel) { this._removeBackground(); createLarge(seriesModel, this.group, true); }, dispose: noop, remove: function (ecModel) { this._clear(ecModel); }, _clear: function (ecModel) { var group = this.group; var data = this._data; if (ecModel && ecModel.get('animation') && data && !this._isLargeDraw) { this._removeBackground(); this._backgroundEls = []; data.eachItemGraphicEl(function (el) { if (el.type === 'sector') { removeSector(el.dataIndex, ecModel, el); } else { removeRect(el.dataIndex, ecModel, el); } }); } else { group.removeAll(); } this._data = null; }, _removeBackground: function () { this.group.remove(this._backgroundGroup); this._backgroundGroup = null; } }); var mathMax$4 = Math.max; var mathMin$4 = Math.min; var clip = { cartesian2d: function (coordSysBoundingRect, layout) { var signWidth = layout.width < 0 ? -1 : 1; var signHeight = layout.height < 0 ? -1 : 1; // Needs positive width and height if (signWidth < 0) { layout.x += layout.width; layout.width = -layout.width; } if (signHeight < 0) { layout.y += layout.height; layout.height = -layout.height; } var x = mathMax$4(layout.x, coordSysBoundingRect.x); var x2 = mathMin$4(layout.x + layout.width, coordSysBoundingRect.x + coordSysBoundingRect.width); var y = mathMax$4(layout.y, coordSysBoundingRect.y); var y2 = mathMin$4(layout.y + layout.height, coordSysBoundingRect.y + coordSysBoundingRect.height); layout.x = x; layout.y = y; layout.width = x2 - x; layout.height = y2 - y; var clipped = layout.width < 0 || layout.height < 0; // Reverse back if (signWidth < 0) { layout.x += layout.width; layout.width = -layout.width; } if (signHeight < 0) { layout.y += layout.height; layout.height = -layout.height; } return clipped; }, polar: function (coordSysClipArea, layout) { var signR = layout.r0 <= layout.r ? 1 : -1; // Make sure r is larger than r0 if (signR < 0) { var r = layout.r; layout.r = layout.r0; layout.r0 = r; } var r = mathMin$4(layout.r, coordSysClipArea.r); var r0 = mathMax$4(layout.r0, coordSysClipArea.r0); layout.r = r; layout.r0 = r0; var clipped = r - r0 < 0; // Reverse back if (signR < 0) { var r = layout.r; layout.r = layout.r0; layout.r0 = r; } return clipped; } }; var elementCreator = { cartesian2d: function ( dataIndex, layout, isHorizontal, animationModel, isUpdate ) { var rect = new Rect({ shape: extend({}, layout), z2: 1 }); rect.name = 'item'; // Animation if (animationModel) { var rectShape = rect.shape; var animateProperty = isHorizontal ? 'height' : 'width'; var animateTarget = {}; rectShape[animateProperty] = 0; animateTarget[animateProperty] = layout[animateProperty]; graphic[isUpdate ? 'updateProps' : 'initProps'](rect, { shape: animateTarget }, animationModel, dataIndex); } return rect; }, polar: function ( dataIndex, layout, isRadial, animationModel, isUpdate, roundCap ) { // Keep the same logic with bar in catesion: use end value to control // direction. Notice that if clockwise is true (by default), the sector // will always draw clockwisely, no matter whether endAngle is greater // or less than startAngle. var clockwise = layout.startAngle < layout.endAngle; var ShapeClass = (!isRadial && roundCap) ? Sausage : Sector; var sector = new ShapeClass({ shape: defaults({clockwise: clockwise}, layout), z2: 1 }); sector.name = 'item'; // Animation if (animationModel) { var sectorShape = sector.shape; var animateProperty = isRadial ? 'r' : 'endAngle'; var animateTarget = {}; sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle; animateTarget[animateProperty] = layout[animateProperty]; graphic[isUpdate ? 'updateProps' : 'initProps'](sector, { shape: animateTarget }, animationModel, dataIndex); } return sector; } }; function removeRect(dataIndex, animationModel, el) { // Not show text when animating el.style.text = null; updateProps(el, { shape: { width: 0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); }); } function removeSector(dataIndex, animationModel, el) { // Not show text when animating el.style.text = null; updateProps(el, { shape: { r: el.shape.r0 } }, animationModel, dataIndex, function () { el.parent && el.parent.remove(el); }); } var getLayout = { // itemModel is only used to get borderWidth, which is not needed // when calculating bar background layout. cartesian2d: function (data, dataIndex, itemModel) { var layout = data.getItemLayout(dataIndex); var fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0; // fix layout with lineWidth var signX = layout.width > 0 ? 1 : -1; var signY = layout.height > 0 ? 1 : -1; return { x: layout.x + signX * fixedLineWidth / 2, y: layout.y + signY * fixedLineWidth / 2, width: layout.width - signX * fixedLineWidth, height: layout.height - signY * fixedLineWidth }; }, polar: function (data, dataIndex, itemModel) { var layout = data.getItemLayout(dataIndex); return { cx: layout.cx, cy: layout.cy, r0: layout.r0, r: layout.r, startAngle: layout.startAngle, endAngle: layout.endAngle }; } }; function isZeroOnPolar(layout) { return layout.startAngle != null && layout.endAngle != null && layout.startAngle === layout.endAngle; } function updateStyle( el, data, dataIndex, itemModel, layout, seriesModel, isHorizontal, isPolar ) { var color = data.getItemVisual(dataIndex, 'color'); var opacity = data.getItemVisual(dataIndex, 'opacity'); var stroke = data.getVisual('borderColor'); var itemStyleModel = itemModel.getModel('itemStyle'); var hoverStyle = itemModel.getModel('emphasis.itemStyle').getBarItemStyle(); if (!isPolar) { el.setShape('r', itemStyleModel.get('barBorderRadius') || 0); } el.useStyle(defaults( { stroke: isZeroOnPolar(layout) ? 'none' : stroke, fill: isZeroOnPolar(layout) ? 'none' : color, opacity: opacity }, itemStyleModel.getBarItemStyle() )); var cursorStyle = itemModel.getShallow('cursor'); cursorStyle && el.attr('cursor', cursorStyle); var labelPositionOutside = isHorizontal ? (layout.height > 0 ? 'bottom' : 'top') : (layout.width > 0 ? 'left' : 'right'); if (!isPolar) { setLabel( el.style, hoverStyle, itemModel, color, seriesModel, dataIndex, labelPositionOutside ); } if (isZeroOnPolar(layout)) { hoverStyle.fill = hoverStyle.stroke = 'none'; } setHoverStyle(el, hoverStyle); } // In case width or height are too small. function getLineWidth(itemModel, rawLayout) { var lineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0; // width or height may be NaN for empty data var width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width); var height = isNaN(rawLayout.height) ? Number.MAX_VALUE : Math.abs(rawLayout.height); return Math.min(lineWidth, width, height); } var LargePath = Path.extend({ type: 'largeBar', shape: {points: []}, buildPath: function (ctx, shape) { // Drawing lines is more efficient than drawing // a whole line or drawing rects. var points = shape.points; var startPoint = this.__startPoint; var baseDimIdx = this.__baseDimIdx; for (var i = 0; i < points.length; i += 2) { startPoint[baseDimIdx] = points[i + baseDimIdx]; ctx.moveTo(startPoint[0], startPoint[1]); ctx.lineTo(points[i], points[i + 1]); } } }); function createLarge(seriesModel, group, incremental) { // TODO support polar var data = seriesModel.getData(); var startPoint = []; var baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0; startPoint[1 - baseDimIdx] = data.getLayout('valueAxisStart'); var largeDataIndices = data.getLayout('largeDataIndices'); var barWidth = data.getLayout('barWidth'); var backgroundModel = seriesModel.getModel('backgroundStyle'); var drawBackground = seriesModel.get('showBackground', true); if (drawBackground) { var points = data.getLayout('largeBackgroundPoints'); var backgroundStartPoint = []; backgroundStartPoint[1 - baseDimIdx] = data.getLayout('backgroundStart'); var bgEl = new LargePath({ shape: {points: points}, incremental: !!incremental, __startPoint: backgroundStartPoint, __baseDimIdx: baseDimIdx, __largeDataIndices: largeDataIndices, __barWidth: barWidth, silent: true, z2: 0 }); setLargeBackgroundStyle(bgEl, backgroundModel, data); group.add(bgEl); } var el = new LargePath({ shape: {points: data.getLayout('largePoints')}, incremental: !!incremental, __startPoint: startPoint, __baseDimIdx: baseDimIdx, __largeDataIndices: largeDataIndices, __barWidth: barWidth }); group.add(el); setLargeStyle(el, seriesModel, data); // Enable tooltip and user mouse/touch event handlers. el.seriesIndex = seriesModel.seriesIndex; if (!seriesModel.get('silent')) { el.on('mousedown', largePathUpdateDataIndex); el.on('mousemove', largePathUpdateDataIndex); } } // Use throttle to avoid frequently traverse to find dataIndex. var largePathUpdateDataIndex = throttle(function (event) { var largePath = this; var dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY); largePath.dataIndex = dataIndex >= 0 ? dataIndex : null; }, 30, false); function largePathFindDataIndex(largePath, x, y) { var baseDimIdx = largePath.__baseDimIdx; var valueDimIdx = 1 - baseDimIdx; var points = largePath.shape.points; var largeDataIndices = largePath.__largeDataIndices; var barWidthHalf = Math.abs(largePath.__barWidth / 2); var startValueVal = largePath.__startPoint[valueDimIdx]; _eventPos[0] = x; _eventPos[1] = y; var pointerBaseVal = _eventPos[baseDimIdx]; var pointerValueVal = _eventPos[1 - baseDimIdx]; var baseLowerBound = pointerBaseVal - barWidthHalf; var baseUpperBound = pointerBaseVal + barWidthHalf; for (var i = 0, len = points.length / 2; i < len; i++) { var ii = i * 2; var barBaseVal = points[ii + baseDimIdx]; var barValueVal = points[ii + valueDimIdx]; if ( barBaseVal >= baseLowerBound && barBaseVal <= baseUpperBound && ( startValueVal <= barValueVal ? (pointerValueVal >= startValueVal && pointerValueVal <= barValueVal) : (pointerValueVal >= barValueVal && pointerValueVal <= startValueVal) ) ) { return largeDataIndices[i]; } } return -1; } function setLargeStyle(el, seriesModel, data) { var borderColor = data.getVisual('borderColor') || data.getVisual('color'); var itemStyle = seriesModel.getModel('itemStyle').getItemStyle(['color', 'borderColor']); el.useStyle(itemStyle); el.style.fill = null; el.style.stroke = borderColor; el.style.lineWidth = data.getLayout('barWidth'); } function setLargeBackgroundStyle(el, backgroundModel, data) { var borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color'); var itemStyle = backgroundModel.getItemStyle(['color', 'borderColor']); el.useStyle(itemStyle); el.style.fill = null; el.style.stroke = borderColor; el.style.lineWidth = data.getLayout('barWidth'); } function createBackgroundShape(isHorizontalOrRadial, layout, coord) { var coordLayout; var isPolar = coord.type === 'polar'; if (isPolar) { coordLayout = coord.getArea(); } else { coordLayout = coord.grid.getRect(); } if (isPolar) { return { cx: coordLayout.cx, cy: coordLayout.cy, r0: isHorizontalOrRadial ? coordLayout.r0 : layout.r0, r: isHorizontalOrRadial ? coordLayout.r : layout.r, startAngle: isHorizontalOrRadial ? layout.startAngle : 0, endAngle: isHorizontalOrRadial ? layout.endAngle : Math.PI * 2 }; } else { return { x: isHorizontalOrRadial ? layout.x : coordLayout.x, y: isHorizontalOrRadial ? coordLayout.y : layout.y, width: isHorizontalOrRadial ? layout.width : coordLayout.width, height: isHorizontalOrRadial ? coordLayout.height : layout.height }; } } function createBackgroundEl(coord, isHorizontalOrRadial, layout) { var ElementClz = coord.type === 'polar' ? Sector : Rect; return new ElementClz({ shape: createBackgroundShape(isHorizontalOrRadial, layout, coord), silent: true, z2: 0 }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // In case developer forget to include grid component registerLayout(PRIORITY.VISUAL.LAYOUT, curry(layout, 'bar')); // Use higher prority to avoid to be blocked by other overall layout, which do not // only exist in this module, but probably also exist in other modules, like `barPolar`. registerLayout(PRIORITY.VISUAL.PROGRESSIVE_LAYOUT, largeLayout); registerVisual({ seriesType: 'bar', reset: function (seriesModel) { // Visual coding for legend seriesModel.getData().setVisual('legendSymbol', 'roundRect'); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * [Usage]: * (1) * createListSimply(seriesModel, ['value']); * (2) * createListSimply(seriesModel, { * coordDimensions: ['value'], * dimensionsCount: 5 * }); * * @param {module:echarts/model/Series} seriesModel * @param {Object|Array.} opt opt or coordDimensions * The options in opt, see `echarts/data/helper/createDimensions` * @param {Array.} [nameList] * @return {module:echarts/data/List} */ var createListSimply = function (seriesModel, opt, nameList) { opt = isArray(opt) && {coordDimensions: opt} || extend({}, opt); var source = seriesModel.getSource(); var dimensionsInfo = createDimensions(source, opt); var list = new List(dimensionsInfo, seriesModel); list.initData(source, nameList); return list; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Data selectable mixin for chart series. * To eanble data select, option of series must have `selectedMode`. * And each data item will use `selected` to toggle itself selected status */ var dataSelectableMixin = { /** * @param {Array.} targetList [{name, value, selected}, ...] * If targetList is an array, it should like [{name: ..., value: ...}, ...]. * If targetList is a "List", it must have coordDim: 'value' dimension and name. */ updateSelectedMap: function (targetList) { this._targetList = isArray(targetList) ? targetList.slice() : []; this._selectTargetMap = reduce(targetList || [], function (targetMap, target) { targetMap.set(target.name, target); return targetMap; }, createHashMap()); }, /** * Either name or id should be passed as input here. * If both of them are defined, id is used. * * @param {string|undefined} name name of data * @param {number|undefined} id dataIndex of data */ // PENGING If selectedMode is null ? select: function (name, id) { var target = id != null ? this._targetList[id] : this._selectTargetMap.get(name); var selectedMode = this.get('selectedMode'); if (selectedMode === 'single') { this._selectTargetMap.each(function (target) { target.selected = false; }); } target && (target.selected = true); }, /** * Either name or id should be passed as input here. * If both of them are defined, id is used. * * @param {string|undefined} name name of data * @param {number|undefined} id dataIndex of data */ unSelect: function (name, id) { var target = id != null ? this._targetList[id] : this._selectTargetMap.get(name); // var selectedMode = this.get('selectedMode'); // selectedMode !== 'single' && target && (target.selected = false); target && (target.selected = false); }, /** * Either name or id should be passed as input here. * If both of them are defined, id is used. * * @param {string|undefined} name name of data * @param {number|undefined} id dataIndex of data */ toggleSelected: function (name, id) { var target = id != null ? this._targetList[id] : this._selectTargetMap.get(name); if (target != null) { this[target.selected ? 'unSelect' : 'select'](name, id); return target.selected; } }, /** * Either name or id should be passed as input here. * If both of them are defined, id is used. * * @param {string|undefined} name name of data * @param {number|undefined} id dataIndex of data */ isSelected: function (name, id) { var target = id != null ? this._targetList[id] : this._selectTargetMap.get(name); return target && target.selected; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * LegendVisualProvider is an bridge that pick encoded color from data and * provide to the legend component. * @param {Function} getDataWithEncodedVisual Function to get data after filtered. It stores all the encoding info * @param {Function} getRawData Function to get raw data before filtered. */ function LegendVisualProvider(getDataWithEncodedVisual, getRawData) { this.getAllNames = function () { var rawData = getRawData(); // We find the name from the raw data. In case it's filtered by the legend component. // Normally, the name can be found in rawData, but can't be found in filtered data will display as gray. return rawData.mapArray(rawData.getName); }; this.containName = function (name) { var rawData = getRawData(); return rawData.indexOfName(name) >= 0; }; this.indexOfName = function (name) { // Only get data when necessary. // Because LegendVisualProvider constructor may be new in the stage that data is not prepared yet. // Invoking Series#getData immediately will throw an error. var dataWithEncodedVisual = getDataWithEncodedVisual(); return dataWithEncodedVisual.indexOfName(name); }; this.getItemVisual = function (dataIndex, key) { // Get encoded visual properties from final filtered data. var dataWithEncodedVisual = getDataWithEncodedVisual(); return dataWithEncodedVisual.getItemVisual(dataIndex, key); }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var PieSeries = extendSeriesModel({ type: 'series.pie', // Overwrite init: function (option) { PieSeries.superApply(this, 'init', arguments); // Enable legend selection for each data item // Use a function instead of direct access because data reference may changed this.legendVisualProvider = new LegendVisualProvider( bind(this.getData, this), bind(this.getRawData, this) ); this.updateSelectedMap(this._createSelectableList()); this._defaultLabelLine(option); }, // Overwrite mergeOption: function (newOption) { PieSeries.superCall(this, 'mergeOption', newOption); this.updateSelectedMap(this._createSelectableList()); }, getInitialData: function (option, ecModel) { return createListSimply(this, { coordDimensions: ['value'], encodeDefaulter: curry(makeSeriesEncodeForNameBased, this) }); }, _createSelectableList: function () { var data = this.getRawData(); var valueDim = data.mapDimension('value'); var targetList = []; for (var i = 0, len = data.count(); i < len; i++) { targetList.push({ name: data.getName(i), value: data.get(valueDim, i), selected: retrieveRawAttr(data, i, 'selected') }); } return targetList; }, // Overwrite getDataParams: function (dataIndex) { var data = this.getData(); var params = PieSeries.superCall(this, 'getDataParams', dataIndex); // FIXME toFixed? var valueList = []; data.each(data.mapDimension('value'), function (value) { valueList.push(value); }); params.percent = getPercentWithPrecision( valueList, dataIndex, data.hostModel.get('percentPrecision') ); params.$vars.push('percent'); return params; }, _defaultLabelLine: function (option) { // Extend labelLine emphasis defaultEmphasis(option, 'labelLine', ['show']); var labelLineNormalOpt = option.labelLine; var labelLineEmphasisOpt = option.emphasis.labelLine; // Not show label line if `label.normal.show = false` labelLineNormalOpt.show = labelLineNormalOpt.show && option.label.show; labelLineEmphasisOpt.show = labelLineEmphasisOpt.show && option.emphasis.label.show; }, defaultOption: { zlevel: 0, z: 2, legendHoverLink: true, hoverAnimation: true, // 默认全局居中 center: ['50%', '50%'], radius: [0, '75%'], // 默认顺时针 clockwise: true, startAngle: 90, // 最小角度改为0 minAngle: 0, // If the angle of a sector less than `minShowLabelAngle`, // the label will not be displayed. minShowLabelAngle: 0, // 选中时扇区偏移量 selectedOffset: 10, // 高亮扇区偏移量 hoverOffset: 10, // If use strategy to avoid label overlapping avoidLabelOverlap: true, // 选择模式,默认关闭,可选single,multiple // selectedMode: false, // 南丁格尔玫瑰图模式,'radius'(半径) | 'area'(面积) // roseType: null, percentPrecision: 2, // If still show when all data zero. stillShowZeroSum: true, // cursor: null, left: 0, top: 0, right: 0, bottom: 0, width: null, height: null, label: { // If rotate around circle rotate: false, show: true, // 'outer', 'inside', 'center' position: 'outer', // 'none', 'labelLine', 'edge'. Works only when position is 'outer' alignTo: 'none', // Closest distance between label and chart edge. // Works only position is 'outer' and alignTo is 'edge'. margin: '25%', // Works only position is 'outer' and alignTo is not 'edge'. bleedMargin: 10, // Distance between text and label line. distanceToLabelLine: 5 // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 // 默认使用全局文本样式,详见TEXTSTYLE // distance: 当position为inner时有效,为label位置到圆心的距离与圆半径(环状图为内外半径和)的比例系数 }, // Enabled when label.normal.position is 'outer' labelLine: { show: true, // 引导线两段中的第一段长度 length: 15, // 引导线两段中的第二段长度 length2: 15, smooth: false, lineStyle: { // color: 各异, width: 1, type: 'solid' } }, itemStyle: { borderWidth: 1 }, // Animation type. Valid values: expansion, scale animationType: 'expansion', // Animation type when update. Valid values: transition, expansion animationTypeUpdate: 'transition', animationEasing: 'cubicOut' } }); mixin(PieSeries, dataSelectableMixin); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {module:echarts/model/Series} seriesModel * @param {boolean} hasAnimation * @inner */ function updateDataSelected(uid, seriesModel, hasAnimation, api) { var data = seriesModel.getData(); var dataIndex = this.dataIndex; var name = data.getName(dataIndex); var selectedOffset = seriesModel.get('selectedOffset'); api.dispatchAction({ type: 'pieToggleSelect', from: uid, name: name, seriesId: seriesModel.id }); data.each(function (idx) { toggleItemSelected( data.getItemGraphicEl(idx), data.getItemLayout(idx), seriesModel.isSelected(data.getName(idx)), selectedOffset, hasAnimation ); }); } /** * @param {module:zrender/graphic/Sector} el * @param {Object} layout * @param {boolean} isSelected * @param {number} selectedOffset * @param {boolean} hasAnimation * @inner */ function toggleItemSelected(el, layout, isSelected, selectedOffset, hasAnimation) { var midAngle = (layout.startAngle + layout.endAngle) / 2; var dx = Math.cos(midAngle); var dy = Math.sin(midAngle); var offset = isSelected ? selectedOffset : 0; var position = [dx * offset, dy * offset]; hasAnimation // animateTo will stop revious animation like update transition ? el.animate() .when(200, { position: position }) .start('bounceOut') : el.attr('position', position); } /** * Piece of pie including Sector, Label, LabelLine * @constructor * @extends {module:zrender/graphic/Group} */ function PiePiece(data, idx) { Group.call(this); var sector = new Sector({ z2: 2 }); var polyline = new Polyline(); var text = new Text(); this.add(sector); this.add(polyline); this.add(text); this.updateData(data, idx, true); } var piePieceProto = PiePiece.prototype; piePieceProto.updateData = function (data, idx, firstCreate) { var sector = this.childAt(0); var labelLine = this.childAt(1); var labelText = this.childAt(2); var seriesModel = data.hostModel; var itemModel = data.getItemModel(idx); var layout = data.getItemLayout(idx); var sectorShape = extend({}, layout); sectorShape.label = null; var animationTypeUpdate = seriesModel.getShallow('animationTypeUpdate'); if (firstCreate) { sector.setShape(sectorShape); var animationType = seriesModel.getShallow('animationType'); if (animationType === 'scale') { sector.shape.r = layout.r0; initProps(sector, { shape: { r: layout.r } }, seriesModel, idx); } // Expansion else { sector.shape.endAngle = layout.startAngle; updateProps(sector, { shape: { endAngle: layout.endAngle } }, seriesModel, idx); } } else { if (animationTypeUpdate === 'expansion') { // Sectors are set to be target shape and an overlaying clipPath is used for animation sector.setShape(sectorShape); } else { // Transition animation from the old shape updateProps(sector, { shape: sectorShape }, seriesModel, idx); } } // Update common style var visualColor = data.getItemVisual(idx, 'color'); sector.useStyle( defaults( { lineJoin: 'bevel', fill: visualColor }, itemModel.getModel('itemStyle').getItemStyle() ) ); sector.hoverStyle = itemModel.getModel('emphasis.itemStyle').getItemStyle(); var cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); // Toggle selected toggleItemSelected( this, data.getItemLayout(idx), seriesModel.isSelected(data.getName(idx)), seriesModel.get('selectedOffset'), seriesModel.get('animation') ); // Label and text animation should be applied only for transition type animation when update var withAnimation = !firstCreate && animationTypeUpdate === 'transition'; this._updateLabel(data, idx, withAnimation); this.highDownOnUpdate = !seriesModel.get('silent') ? function (fromState, toState) { var hasAnimation = seriesModel.isAnimationEnabled() && itemModel.get('hoverAnimation'); if (toState === 'emphasis') { labelLine.ignore = labelLine.hoverIgnore; labelText.ignore = labelText.hoverIgnore; // Sector may has animation of updating data. Force to move to the last frame // Or it may stopped on the wrong shape if (hasAnimation) { sector.stopAnimation(true); sector.animateTo({ shape: { r: layout.r + seriesModel.get('hoverOffset') } }, 300, 'elasticOut'); } } else { labelLine.ignore = labelLine.normalIgnore; labelText.ignore = labelText.normalIgnore; if (hasAnimation) { sector.stopAnimation(true); sector.animateTo({ shape: { r: layout.r } }, 300, 'elasticOut'); } } } : null; setHoverStyle(this); }; piePieceProto._updateLabel = function (data, idx, withAnimation) { var labelLine = this.childAt(1); var labelText = this.childAt(2); var seriesModel = data.hostModel; var itemModel = data.getItemModel(idx); var layout = data.getItemLayout(idx); var labelLayout = layout.label; var visualColor = data.getItemVisual(idx, 'color'); if (!labelLayout || isNaN(labelLayout.x) || isNaN(labelLayout.y)) { labelText.ignore = labelText.normalIgnore = labelText.hoverIgnore = labelLine.ignore = labelLine.normalIgnore = labelLine.hoverIgnore = true; return; } var targetLineShape = { points: labelLayout.linePoints || [ [labelLayout.x, labelLayout.y], [labelLayout.x, labelLayout.y], [labelLayout.x, labelLayout.y] ] }; var targetTextStyle = { x: labelLayout.x, y: labelLayout.y }; if (withAnimation) { updateProps(labelLine, { shape: targetLineShape }, seriesModel, idx); updateProps(labelText, { style: targetTextStyle }, seriesModel, idx); } else { labelLine.attr({ shape: targetLineShape }); labelText.attr({ style: targetTextStyle }); } labelText.attr({ rotation: labelLayout.rotation, origin: [labelLayout.x, labelLayout.y], z2: 10 }); var labelModel = itemModel.getModel('label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var labelLineModel = itemModel.getModel('labelLine'); var labelLineHoverModel = itemModel.getModel('emphasis.labelLine'); var visualColor = data.getItemVisual(idx, 'color'); setLabelStyle( labelText.style, labelText.hoverStyle = {}, labelModel, labelHoverModel, { labelFetcher: data.hostModel, labelDataIndex: idx, defaultText: labelLayout.text, autoColor: visualColor, useInsideStyle: !!labelLayout.inside }, { textAlign: labelLayout.textAlign, textVerticalAlign: labelLayout.verticalAlign, opacity: data.getItemVisual(idx, 'opacity') } ); labelText.ignore = labelText.normalIgnore = !labelModel.get('show'); labelText.hoverIgnore = !labelHoverModel.get('show'); labelLine.ignore = labelLine.normalIgnore = !labelLineModel.get('show'); labelLine.hoverIgnore = !labelLineHoverModel.get('show'); // Default use item visual color labelLine.setStyle({ stroke: visualColor, opacity: data.getItemVisual(idx, 'opacity') }); labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); labelLine.hoverStyle = labelLineHoverModel.getModel('lineStyle').getLineStyle(); var smooth = labelLineModel.get('smooth'); if (smooth && smooth === true) { smooth = 0.4; } labelLine.setShape({ smooth: smooth }); }; inherits(PiePiece, Group); // Pie view var PieView = Chart.extend({ type: 'pie', init: function () { var sectorGroup = new Group(); this._sectorGroup = sectorGroup; }, render: function (seriesModel, ecModel, api, payload) { if (payload && (payload.from === this.uid)) { return; } var data = seriesModel.getData(); var oldData = this._data; var group = this.group; var hasAnimation = ecModel.get('animation'); var isFirstRender = !oldData; var animationType = seriesModel.get('animationType'); var animationTypeUpdate = seriesModel.get('animationTypeUpdate'); var onSectorClick = curry( updateDataSelected, this.uid, seriesModel, hasAnimation, api ); var selectedMode = seriesModel.get('selectedMode'); data.diff(oldData) .add(function (idx) { var piePiece = new PiePiece(data, idx); // Default expansion animation if (isFirstRender && animationType !== 'scale') { piePiece.eachChild(function (child) { child.stopAnimation(true); }); } selectedMode && piePiece.on('click', onSectorClick); data.setItemGraphicEl(idx, piePiece); group.add(piePiece); }) .update(function (newIdx, oldIdx) { var piePiece = oldData.getItemGraphicEl(oldIdx); if (!isFirstRender && animationTypeUpdate !== 'transition') { piePiece.eachChild(function (child) { child.stopAnimation(true); }); } piePiece.updateData(data, newIdx); piePiece.off('click'); selectedMode && piePiece.on('click', onSectorClick); group.add(piePiece); data.setItemGraphicEl(newIdx, piePiece); }) .remove(function (idx) { var piePiece = oldData.getItemGraphicEl(idx); group.remove(piePiece); }) .execute(); if ( hasAnimation && data.count() > 0 && (isFirstRender ? animationType !== 'scale' : animationTypeUpdate !== 'transition') ) { var shape = data.getItemLayout(0); for (var s = 1; isNaN(shape.startAngle) && s < data.count(); ++s) { shape = data.getItemLayout(s); } var r = Math.max(api.getWidth(), api.getHeight()) / 2; var removeClipPath = bind(group.removeClipPath, group); group.setClipPath(this._createClipPath( shape.cx, shape.cy, r, shape.startAngle, shape.clockwise, removeClipPath, seriesModel, isFirstRender )); } else { // clipPath is used in first-time animation, so remove it when otherwise. See: #8994 group.removeClipPath(); } this._data = data; }, dispose: function () {}, _createClipPath: function ( cx, cy, r, startAngle, clockwise, cb, seriesModel, isFirstRender ) { var clipPath = new Sector({ shape: { cx: cx, cy: cy, r0: 0, r: r, startAngle: startAngle, endAngle: startAngle, clockwise: clockwise } }); var initOrUpdate = isFirstRender ? initProps : updateProps; initOrUpdate(clipPath, { shape: { endAngle: startAngle + (clockwise ? 1 : -1) * Math.PI * 2 } }, seriesModel, cb); return clipPath; }, /** * @implement */ containPoint: function (point, seriesModel) { var data = seriesModel.getData(); var itemLayout = data.getItemLayout(0); if (itemLayout) { var dx = point[0] - itemLayout.cx; var dy = point[1] - itemLayout.cy; var radius = Math.sqrt(dx * dx + dy * dy); return radius <= itemLayout.r && radius >= itemLayout.r0; } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var createDataSelectAction = function (seriesType, actionInfos) { each$1(actionInfos, function (actionInfo) { actionInfo.update = 'updateView'; /** * @payload * @property {string} seriesName * @property {string} name */ registerAction(actionInfo, function (payload, ecModel) { var selected = {}; ecModel.eachComponent( {mainType: 'series', subType: seriesType, query: payload}, function (seriesModel) { if (seriesModel[actionInfo.method]) { seriesModel[actionInfo.method]( payload.name, payload.dataIndex ); } var data = seriesModel.getData(); // Create selected map data.each(function (idx) { var name = data.getName(idx); selected[name] = seriesModel.isSelected(name) || false; }); } ); return { name: payload.name, selected: selected, seriesId: payload.seriesId }; }); }); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Pick color from palette for each data item. // Applicable for charts that require applying color palette // in data level (like pie, funnel, chord). var dataColor = function (seriesType) { return { getTargetSeries: function (ecModel) { // Pie and funnel may use diferrent scope var paletteScope = {}; var seiresModelMap = createHashMap(); ecModel.eachSeriesByType(seriesType, function (seriesModel) { seriesModel.__paletteScope = paletteScope; seiresModelMap.set(seriesModel.uid, seriesModel); }); return seiresModelMap; }, reset: function (seriesModel, ecModel) { var dataAll = seriesModel.getRawData(); var idxMap = {}; var data = seriesModel.getData(); data.each(function (idx) { var rawIdx = data.getRawIndex(idx); idxMap[rawIdx] = idx; }); dataAll.each(function (rawIdx) { var filteredIdx = idxMap[rawIdx]; // If series.itemStyle.normal.color is a function. itemVisual may be encoded var singleDataColor = filteredIdx != null && data.getItemVisual(filteredIdx, 'color', true); var singleDataBorderColor = filteredIdx != null && data.getItemVisual(filteredIdx, 'borderColor', true); var itemModel; if (!singleDataColor || !singleDataBorderColor) { // FIXME Performance itemModel = dataAll.getItemModel(rawIdx); } if (!singleDataColor) { var color = itemModel.get('itemStyle.color') || seriesModel.getColorFromPalette( dataAll.getName(rawIdx) || (rawIdx + ''), seriesModel.__paletteScope, dataAll.count() ); // Data is not filtered if (filteredIdx != null) { data.setItemVisual(filteredIdx, 'color', color); } } if (!singleDataBorderColor) { var borderColor = itemModel.get('itemStyle.borderColor'); // Data is not filtered if (filteredIdx != null) { data.setItemVisual(filteredIdx, 'borderColor', borderColor); } } }); } }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // FIXME emphasis label position is not same with normal label position var RADIAN$1 = Math.PI / 180; function adjustSingleSide(list, cx, cy, r, dir, viewWidth, viewHeight, viewLeft, viewTop, farthestX) { list.sort(function (a, b) { return a.y - b.y; }); function shiftDown(start, end, delta, dir) { for (var j = start; j < end; j++) { if (list[j].y + delta > viewTop + viewHeight) { break; } list[j].y += delta; if (j > start && j + 1 < end && list[j + 1].y > list[j].y + list[j].height ) { shiftUp(j, delta / 2); return; } } shiftUp(end - 1, delta / 2); } function shiftUp(end, delta) { for (var j = end; j >= 0; j--) { if (list[j].y - delta < viewTop) { break; } list[j].y -= delta; if (j > 0 && list[j].y > list[j - 1].y + list[j - 1].height ) { break; } } } function changeX(list, isDownList, cx, cy, r, dir) { var lastDeltaX = dir > 0 ? isDownList // right-side ? Number.MAX_VALUE // down : 0 // up : isDownList // left-side ? Number.MAX_VALUE // down : 0; // up for (var i = 0, l = list.length; i < l; i++) { if (list[i].labelAlignTo !== 'none') { continue; } var deltaY = Math.abs(list[i].y - cy); var length = list[i].len; var length2 = list[i].len2; var deltaX = (deltaY < r + length) ? Math.sqrt( (r + length + length2) * (r + length + length2) - deltaY * deltaY ) : Math.abs(list[i].x - cx); if (isDownList && deltaX >= lastDeltaX) { // right-down, left-down deltaX = lastDeltaX - 10; } if (!isDownList && deltaX <= lastDeltaX) { // right-up, left-up deltaX = lastDeltaX + 10; } list[i].x = cx + deltaX * dir; lastDeltaX = deltaX; } } var lastY = 0; var delta; var len = list.length; var upList = []; var downList = []; for (var i = 0; i < len; i++) { if (list[i].position === 'outer' && list[i].labelAlignTo === 'labelLine') { var dx = list[i].x - farthestX; list[i].linePoints[1][0] += dx; list[i].x = farthestX; } delta = list[i].y - lastY; if (delta < 0) { shiftDown(i, len, -delta, dir); } lastY = list[i].y + list[i].height; } if (viewHeight - lastY < 0) { shiftUp(len - 1, lastY - viewHeight); } for (var i = 0; i < len; i++) { if (list[i].y >= cy) { downList.push(list[i]); } else { upList.push(list[i]); } } changeX(upList, false, cx, cy, r, dir); changeX(downList, true, cx, cy, r, dir); } function avoidOverlap(labelLayoutList, cx, cy, r, viewWidth, viewHeight, viewLeft, viewTop) { var leftList = []; var rightList = []; var leftmostX = Number.MAX_VALUE; var rightmostX = -Number.MAX_VALUE; for (var i = 0; i < labelLayoutList.length; i++) { if (isPositionCenter(labelLayoutList[i])) { continue; } if (labelLayoutList[i].x < cx) { leftmostX = Math.min(leftmostX, labelLayoutList[i].x); leftList.push(labelLayoutList[i]); } else { rightmostX = Math.max(rightmostX, labelLayoutList[i].x); rightList.push(labelLayoutList[i]); } } adjustSingleSide(rightList, cx, cy, r, 1, viewWidth, viewHeight, viewLeft, viewTop, rightmostX); adjustSingleSide(leftList, cx, cy, r, -1, viewWidth, viewHeight, viewLeft, viewTop, leftmostX); for (var i = 0; i < labelLayoutList.length; i++) { var layout = labelLayoutList[i]; if (isPositionCenter(layout)) { continue; } var linePoints = layout.linePoints; if (linePoints) { var isAlignToEdge = layout.labelAlignTo === 'edge'; var realTextWidth = layout.textRect.width; var targetTextWidth; if (isAlignToEdge) { if (layout.x < cx) { targetTextWidth = linePoints[2][0] - layout.labelDistance - viewLeft - layout.labelMargin; } else { targetTextWidth = viewLeft + viewWidth - layout.labelMargin - linePoints[2][0] - layout.labelDistance; } } else { if (layout.x < cx) { targetTextWidth = layout.x - viewLeft - layout.bleedMargin; } else { targetTextWidth = viewLeft + viewWidth - layout.x - layout.bleedMargin; } } if (targetTextWidth < layout.textRect.width) { layout.text = truncateText(layout.text, targetTextWidth, layout.font); if (layout.labelAlignTo === 'edge') { realTextWidth = getWidth(layout.text, layout.font); } } var dist = linePoints[1][0] - linePoints[2][0]; if (isAlignToEdge) { if (layout.x < cx) { linePoints[2][0] = viewLeft + layout.labelMargin + realTextWidth + layout.labelDistance; } else { linePoints[2][0] = viewLeft + viewWidth - layout.labelMargin - realTextWidth - layout.labelDistance; } } else { if (layout.x < cx) { linePoints[2][0] = layout.x + layout.labelDistance; } else { linePoints[2][0] = layout.x - layout.labelDistance; } linePoints[1][0] = linePoints[2][0] + dist; } linePoints[1][1] = linePoints[2][1] = layout.y; } } } function isPositionCenter(layout) { // Not change x for center label return layout.position === 'center'; } var labelLayout = function (seriesModel, r, viewWidth, viewHeight, viewLeft, viewTop) { var data = seriesModel.getData(); var labelLayoutList = []; var cx; var cy; var hasLabelRotate = false; var minShowLabelRadian = (seriesModel.get('minShowLabelAngle') || 0) * RADIAN$1; data.each(function (idx) { var layout = data.getItemLayout(idx); var itemModel = data.getItemModel(idx); var labelModel = itemModel.getModel('label'); // Use position in normal or emphasis var labelPosition = labelModel.get('position') || itemModel.get('emphasis.label.position'); var labelDistance = labelModel.get('distanceToLabelLine'); var labelAlignTo = labelModel.get('alignTo'); var labelMargin = parsePercent$1(labelModel.get('margin'), viewWidth); var bleedMargin = labelModel.get('bleedMargin'); var font = labelModel.getFont(); var labelLineModel = itemModel.getModel('labelLine'); var labelLineLen = labelLineModel.get('length'); labelLineLen = parsePercent$1(labelLineLen, viewWidth); var labelLineLen2 = labelLineModel.get('length2'); labelLineLen2 = parsePercent$1(labelLineLen2, viewWidth); if (layout.angle < minShowLabelRadian) { return; } var midAngle = (layout.startAngle + layout.endAngle) / 2; var dx = Math.cos(midAngle); var dy = Math.sin(midAngle); var textX; var textY; var linePoints; var textAlign; cx = layout.cx; cy = layout.cy; var text = seriesModel.getFormattedLabel(idx, 'normal') || data.getName(idx); var textRect = getBoundingRect( text, font, textAlign, 'top' ); var isLabelInside = labelPosition === 'inside' || labelPosition === 'inner'; if (labelPosition === 'center') { textX = layout.cx; textY = layout.cy; textAlign = 'center'; } else { var x1 = (isLabelInside ? (layout.r + layout.r0) / 2 * dx : layout.r * dx) + cx; var y1 = (isLabelInside ? (layout.r + layout.r0) / 2 * dy : layout.r * dy) + cy; textX = x1 + dx * 3; textY = y1 + dy * 3; if (!isLabelInside) { // For roseType var x2 = x1 + dx * (labelLineLen + r - layout.r); var y2 = y1 + dy * (labelLineLen + r - layout.r); var x3 = x2 + ((dx < 0 ? -1 : 1) * labelLineLen2); var y3 = y2; if (labelAlignTo === 'edge') { // Adjust textX because text align of edge is opposite textX = dx < 0 ? viewLeft + labelMargin : viewLeft + viewWidth - labelMargin; } else { textX = x3 + (dx < 0 ? -labelDistance : labelDistance); } textY = y3; linePoints = [[x1, y1], [x2, y2], [x3, y3]]; } textAlign = isLabelInside ? 'center' : (labelAlignTo === 'edge' ? (dx > 0 ? 'right' : 'left') : (dx > 0 ? 'left' : 'right')); } var labelRotate; var rotate = labelModel.get('rotate'); if (typeof rotate === 'number') { labelRotate = rotate * (Math.PI / 180); } else { labelRotate = rotate ? (dx < 0 ? -midAngle + Math.PI : -midAngle) : 0; } hasLabelRotate = !!labelRotate; layout.label = { x: textX, y: textY, position: labelPosition, height: textRect.height, len: labelLineLen, len2: labelLineLen2, linePoints: linePoints, textAlign: textAlign, verticalAlign: 'middle', rotation: labelRotate, inside: isLabelInside, labelDistance: labelDistance, labelAlignTo: labelAlignTo, labelMargin: labelMargin, bleedMargin: bleedMargin, textRect: textRect, text: text, font: font }; // Not layout the inside label if (!isLabelInside) { labelLayoutList.push(layout.label); } }); if (!hasLabelRotate && seriesModel.get('avoidLabelOverlap')) { avoidOverlap(labelLayoutList, cx, cy, r, viewWidth, viewHeight, viewLeft, viewTop); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var PI2$4 = Math.PI * 2; var RADIAN = Math.PI / 180; function getViewRect(seriesModel, api) { return getLayoutRect( seriesModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() } ); } var pieLayout = function (seriesType, ecModel, api, payload) { ecModel.eachSeriesByType(seriesType, function (seriesModel) { var data = seriesModel.getData(); var valueDim = data.mapDimension('value'); var viewRect = getViewRect(seriesModel, api); var center = seriesModel.get('center'); var radius = seriesModel.get('radius'); if (!isArray(radius)) { radius = [0, radius]; } if (!isArray(center)) { center = [center, center]; } var width = parsePercent$1(viewRect.width, api.getWidth()); var height = parsePercent$1(viewRect.height, api.getHeight()); var size = Math.min(width, height); var cx = parsePercent$1(center[0], width) + viewRect.x; var cy = parsePercent$1(center[1], height) + viewRect.y; var r0 = parsePercent$1(radius[0], size / 2); var r = parsePercent$1(radius[1], size / 2); var startAngle = -seriesModel.get('startAngle') * RADIAN; var minAngle = seriesModel.get('minAngle') * RADIAN; var validDataCount = 0; data.each(valueDim, function (value) { !isNaN(value) && validDataCount++; }); var sum = data.getSum(valueDim); // Sum may be 0 var unitRadian = Math.PI / (sum || validDataCount) * 2; var clockwise = seriesModel.get('clockwise'); var roseType = seriesModel.get('roseType'); var stillShowZeroSum = seriesModel.get('stillShowZeroSum'); // [0...max] var extent = data.getDataExtent(valueDim); extent[0] = 0; // In the case some sector angle is smaller than minAngle var restAngle = PI2$4; var valueSumLargerThanMinAngle = 0; var currentAngle = startAngle; var dir = clockwise ? 1 : -1; data.each(valueDim, function (value, idx) { var angle; if (isNaN(value)) { data.setItemLayout(idx, { angle: NaN, startAngle: NaN, endAngle: NaN, clockwise: clockwise, cx: cx, cy: cy, r0: r0, r: roseType ? NaN : r, viewRect: viewRect }); return; } // FIXME 兼容 2.0 但是 roseType 是 area 的时候才是这样? if (roseType !== 'area') { angle = (sum === 0 && stillShowZeroSum) ? unitRadian : (value * unitRadian); } else { angle = PI2$4 / validDataCount; } if (angle < minAngle) { angle = minAngle; restAngle -= minAngle; } else { valueSumLargerThanMinAngle += value; } var endAngle = currentAngle + dir * angle; data.setItemLayout(idx, { angle: angle, startAngle: currentAngle, endAngle: endAngle, clockwise: clockwise, cx: cx, cy: cy, r0: r0, r: roseType ? linearMap(value, extent, [r0, r]) : r, viewRect: viewRect }); currentAngle = endAngle; }); // Some sector is constrained by minAngle // Rest sectors needs recalculate angle if (restAngle < PI2$4 && validDataCount) { // Average the angle if rest angle is not enough after all angles is // Constrained by minAngle if (restAngle <= 1e-3) { var angle = PI2$4 / validDataCount; data.each(valueDim, function (value, idx) { if (!isNaN(value)) { var layout = data.getItemLayout(idx); layout.angle = angle; layout.startAngle = startAngle + dir * idx * angle; layout.endAngle = startAngle + dir * (idx + 1) * angle; } }); } else { unitRadian = restAngle / valueSumLargerThanMinAngle; currentAngle = startAngle; data.each(valueDim, function (value, idx) { if (!isNaN(value)) { var layout = data.getItemLayout(idx); var angle = layout.angle === minAngle ? minAngle : value * unitRadian; layout.startAngle = currentAngle; layout.endAngle = currentAngle + dir * angle; currentAngle += dir * angle; } }); } } labelLayout(seriesModel, r, viewRect.width, viewRect.height, viewRect.x, viewRect.y); }); }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var dataFilter = function (seriesType) { return { seriesType: seriesType, reset: function (seriesModel, ecModel) { var legendModels = ecModel.findComponents({ mainType: 'legend' }); if (!legendModels || !legendModels.length) { return; } var data = seriesModel.getData(); data.filterSelf(function (idx) { var name = data.getName(idx); // If in any legend component the status is not selected. for (var i = 0; i < legendModels.length; i++) { if (!legendModels[i].isSelected(name)) { return false; } } return true; }); } }; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ createDataSelectAction('pie', [{ type: 'pieToggleSelect', event: 'pieselectchanged', method: 'toggleSelected' }, { type: 'pieSelect', event: 'pieselected', method: 'select' }, { type: 'pieUnSelect', event: 'pieunselected', method: 'unSelect' }]); registerVisual(dataColor('pie')); registerLayout(curry(pieLayout, 'pie')); registerProcessor(dataFilter('pie')); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ SeriesModel.extend({ type: 'series.scatter', dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'], getInitialData: function (option, ecModel) { return createListFromArray(this.getSource(), this, {useEncodeDefaulter: true}); }, brushSelector: 'point', getProgressive: function () { var progressive = this.option.progressive; if (progressive == null) { // PENDING return this.option.large ? 5e3 : this.get('progressive'); } return progressive; }, getProgressiveThreshold: function () { var progressiveThreshold = this.option.progressiveThreshold; if (progressiveThreshold == null) { // PENDING return this.option.large ? 1e4 : this.get('progressiveThreshold'); } return progressiveThreshold; }, defaultOption: { coordinateSystem: 'cartesian2d', zlevel: 0, z: 2, legendHoverLink: true, hoverAnimation: true, // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, // Polar coordinate system // polarIndex: 0, // Geo coordinate system // geoIndex: 0, // symbol: null, // 图形类型 symbolSize: 10, // 图形大小,半宽(半径)参数,当图形为方向或菱形则总宽度为symbolSize * 2 // symbolRotate: null, // 图形旋转控制 large: false, // Available when large is true largeThreshold: 2000, // cursor: null, // label: { // show: false // distance: 5, // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 // position: 默认自适应,水平布局为'top',垂直布局为'right',可选为 // 'inside'|'left'|'right'|'top'|'bottom' // 默认使用全局文本样式,详见TEXTSTYLE // }, itemStyle: { opacity: 0.8 // color: 各异 }, // If clip the overflow graphics // Works on cartesian / polar series clip: true // progressive: null } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global Float32Array */ // TODO Batch by color var BOOST_SIZE_THRESHOLD = 4; var LargeSymbolPath = extendShape({ shape: { points: null }, symbolProxy: null, softClipShape: null, buildPath: function (path, shape) { var points = shape.points; var size = shape.size; var symbolProxy = this.symbolProxy; var symbolProxyShape = symbolProxy.shape; var ctx = path.getContext ? path.getContext() : path; var canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD; // Do draw in afterBrush. if (canBoost) { return; } for (var i = 0; i < points.length;) { var x = points[i++]; var y = points[i++]; if (isNaN(x) || isNaN(y)) { continue; } if (this.softClipShape && !this.softClipShape.contain(x, y)) { continue; } symbolProxyShape.x = x - size[0] / 2; symbolProxyShape.y = y - size[1] / 2; symbolProxyShape.width = size[0]; symbolProxyShape.height = size[1]; symbolProxy.buildPath(path, symbolProxyShape, true); } }, afterBrush: function (ctx) { var shape = this.shape; var points = shape.points; var size = shape.size; var canBoost = size[0] < BOOST_SIZE_THRESHOLD; if (!canBoost) { return; } this.setTransform(ctx); // PENDING If style or other canvas status changed? for (var i = 0; i < points.length;) { var x = points[i++]; var y = points[i++]; if (isNaN(x) || isNaN(y)) { continue; } if (this.softClipShape && !this.softClipShape.contain(x, y)) { continue; } // fillRect is faster than building a rect path and draw. // And it support light globalCompositeOperation. ctx.fillRect( x - size[0] / 2, y - size[1] / 2, size[0], size[1] ); } this.restoreTransform(ctx); }, findDataIndex: function (x, y) { // TODO ??? // Consider transform var shape = this.shape; var points = shape.points; var size = shape.size; var w = Math.max(size[0], 4); var h = Math.max(size[1], 4); // Not consider transform // Treat each element as a rect // top down traverse for (var idx = points.length / 2 - 1; idx >= 0; idx--) { var i = idx * 2; var x0 = points[i] - w / 2; var y0 = points[i + 1] - h / 2; if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) { return idx; } } return -1; } }); function LargeSymbolDraw() { this.group = new Group(); } var largeSymbolProto = LargeSymbolDraw.prototype; largeSymbolProto.isPersistent = function () { return !this._incremental; }; /** * Update symbols draw by new data * @param {module:echarts/data/List} data * @param {Object} opt * @param {Object} [opt.clipShape] */ largeSymbolProto.updateData = function (data, opt) { this.group.removeAll(); var symbolEl = new LargeSymbolPath({ rectHover: true, cursor: 'default' }); symbolEl.setShape({ points: data.getLayout('symbolPoints') }); this._setCommon(symbolEl, data, false, opt); this.group.add(symbolEl); this._incremental = null; }; largeSymbolProto.updateLayout = function (data) { if (this._incremental) { return; } var points = data.getLayout('symbolPoints'); this.group.eachChild(function (child) { if (child.startIndex != null) { var len = (child.endIndex - child.startIndex) * 2; var byteOffset = child.startIndex * 4 * 2; points = new Float32Array(points.buffer, byteOffset, len); } child.setShape('points', points); }); }; largeSymbolProto.incrementalPrepareUpdate = function (data) { this.group.removeAll(); this._clearIncremental(); // Only use incremental displayables when data amount is larger than 2 million. // PENDING Incremental data? if (data.count() > 2e6) { if (!this._incremental) { this._incremental = new IncrementalDisplayble({ silent: true }); } this.group.add(this._incremental); } else { this._incremental = null; } }; largeSymbolProto.incrementalUpdate = function (taskParams, data, opt) { var symbolEl; if (this._incremental) { symbolEl = new LargeSymbolPath(); this._incremental.addDisplayable(symbolEl, true); } else { symbolEl = new LargeSymbolPath({ rectHover: true, cursor: 'default', startIndex: taskParams.start, endIndex: taskParams.end }); symbolEl.incremental = true; this.group.add(symbolEl); } symbolEl.setShape({ points: data.getLayout('symbolPoints') }); this._setCommon(symbolEl, data, !!this._incremental, opt); }; largeSymbolProto._setCommon = function (symbolEl, data, isIncremental, opt) { var hostModel = data.hostModel; opt = opt || {}; // TODO // if (data.hasItemVisual.symbolSize) { // // TODO typed array? // symbolEl.setShape('sizes', data.mapArray( // function (idx) { // var size = data.getItemVisual(idx, 'symbolSize'); // return (size instanceof Array) ? size : [size, size]; // } // )); // } // else { var size = data.getVisual('symbolSize'); symbolEl.setShape('size', (size instanceof Array) ? size : [size, size]); // } symbolEl.softClipShape = opt.clipShape || null; // Create symbolProxy to build path for each data symbolEl.symbolProxy = createSymbol( data.getVisual('symbol'), 0, 0, 0, 0 ); // Use symbolProxy setColor method symbolEl.setColor = symbolEl.symbolProxy.setColor; var extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD; symbolEl.useStyle( // Draw shadow when doing fillRect is extremely slow. hostModel.getModel('itemStyle').getItemStyle(extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color']) ); var visualColor = data.getVisual('color'); if (visualColor) { symbolEl.setColor(visualColor); } if (!isIncremental) { // Enable tooltip // PENDING May have performance issue when path is extremely large symbolEl.seriesIndex = hostModel.seriesIndex; symbolEl.on('mousemove', function (e) { symbolEl.dataIndex = null; var dataIndex = symbolEl.findDataIndex(e.offsetX, e.offsetY); if (dataIndex >= 0) { // Provide dataIndex for tooltip symbolEl.dataIndex = dataIndex + (symbolEl.startIndex || 0); } }); } }; largeSymbolProto.remove = function () { this._clearIncremental(); this._incremental = null; this.group.removeAll(); }; largeSymbolProto._clearIncremental = function () { var incremental = this._incremental; if (incremental) { incremental.clearDisplaybles(); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ extendChartView({ type: 'scatter', render: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var symbolDraw = this._updateSymbolDraw(data, seriesModel); symbolDraw.updateData(data, { // TODO // If this parameter should be a shape or a bounding volume // shape will be more general. // But bounding volume like bounding rect will be much faster in the contain calculation clipShape: this._getClipShape(seriesModel) }); this._finished = true; }, incrementalPrepareRender: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var symbolDraw = this._updateSymbolDraw(data, seriesModel); symbolDraw.incrementalPrepareUpdate(data); this._finished = false; }, incrementalRender: function (taskParams, seriesModel, ecModel) { this._symbolDraw.incrementalUpdate(taskParams, seriesModel.getData(), { clipShape: this._getClipShape(seriesModel) }); this._finished = taskParams.end === seriesModel.getData().count(); }, updateTransform: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); // Must mark group dirty and make sure the incremental layer will be cleared // PENDING this.group.dirty(); if (!this._finished || data.count() > 1e4 || !this._symbolDraw.isPersistent()) { return { update: true }; } else { var res = pointsLayout().reset(seriesModel); if (res.progress) { res.progress({ start: 0, end: data.count() }, data); } this._symbolDraw.updateLayout(data); } }, _getClipShape: function (seriesModel) { var coordSys = seriesModel.coordinateSystem; var clipArea = coordSys && coordSys.getArea && coordSys.getArea(); return seriesModel.get('clip', true) ? clipArea : null; }, _updateSymbolDraw: function (data, seriesModel) { var symbolDraw = this._symbolDraw; var pipelineContext = seriesModel.pipelineContext; var isLargeDraw = pipelineContext.large; if (!symbolDraw || isLargeDraw !== this._isLargeDraw) { symbolDraw && symbolDraw.remove(); symbolDraw = this._symbolDraw = isLargeDraw ? new LargeSymbolDraw() : new SymbolDraw(); this._isLargeDraw = isLargeDraw; this.group.removeAll(); } this.group.add(symbolDraw.group); return symbolDraw; }, remove: function (ecModel, api) { this._symbolDraw && this._symbolDraw.remove(true); this._symbolDraw = null; }, dispose: function () {} }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import * as zrUtil from 'zrender/src/core/util'; // In case developer forget to include grid component registerVisual(visualSymbol('scatter', 'circle')); registerLayout(pointsLayout('scatter')); // echarts.registerProcessor(function (ecModel, api) { // ecModel.eachSeriesByType('scatter', function (seriesModel) { // var data = seriesModel.getData(); // var coordSys = seriesModel.coordinateSystem; // if (coordSys.type !== 'geo') { // return; // } // var startPt = coordSys.pointToData([0, 0]); // var endPt = coordSys.pointToData([api.getWidth(), api.getHeight()]); // var dims = zrUtil.map(coordSys.dimensions, function (dim) { // return data.mapDimension(dim); // }); // var range = {}; // range[dims[0]] = [Math.min(startPt[0], endPt[0]), Math.max(startPt[0], endPt[0])]; // range[dims[1]] = [Math.min(startPt[1], endPt[1]), Math.max(startPt[1], endPt[1])]; // data.selectRange(range); // }); // }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var _nonShapeGraphicElements = { // Reserved but not supported in graphic component. path: null, compoundPath: null, // Supported in graphic component. group: Group, image: ZImage, text: Text }; // ------------- // Preprocessor // ------------- registerPreprocessor(function (option) { var graphicOption = option.graphic; // Convert // {graphic: [{left: 10, type: 'circle'}, ...]} // or // {graphic: {left: 10, type: 'circle'}} // to // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} if (isArray(graphicOption)) { if (!graphicOption[0] || !graphicOption[0].elements) { option.graphic = [{elements: graphicOption}]; } else { // Only one graphic instance can be instantiated. (We dont // want that too many views are created in echarts._viewMap) option.graphic = [option.graphic[0]]; } } else if (graphicOption && !graphicOption.elements) { option.graphic = [{elements: [graphicOption]}]; } }); // ------ // Model // ------ var GraphicModel = extendComponentModel({ type: 'graphic', defaultOption: { // Extra properties for each elements: // // left/right/top/bottom: (like 12, '22%', 'center', default undefined) // If left/rigth is set, shape.x/shape.cx/position will not be used. // If top/bottom is set, shape.y/shape.cy/position will not be used. // This mechanism is useful when you want to position a group/element // against the right side or the center of this container. // // width/height: (can only be pixel value, default 0) // Only be used to specify contianer(group) size, if needed. And // can not be percentage value (like '33%'). See the reason in the // layout algorithm below. // // bounding: (enum: 'all' (default) | 'raw') // Specify how to calculate boundingRect when locating. // 'all': Get uioned and transformed boundingRect // from both itself and its descendants. // This mode simplies confining a group of elements in the bounding // of their ancester container (e.g., using 'right: 0'). // 'raw': Only use the boundingRect of itself and before transformed. // This mode is similar to css behavior, which is useful when you // want an element to be able to overflow its container. (Consider // a rotated circle needs to be located in a corner.) // info: custom info. enables user to mount some info on elements and use them // in event handlers. Update them only when user specified, otherwise, remain. // Note: elements is always behind its ancestors in this elements array. elements: [], parentId: null }, /** * Save el options for the sake of the performance (only update modified graphics). * The order is the same as those in option. (ancesters -> descendants) * * @private * @type {Array.} */ _elOptionsToUpdate: null, /** * @override */ mergeOption: function (option) { // Prevent default merge to elements var elements = this.option.elements; this.option.elements = null; GraphicModel.superApply(this, 'mergeOption', arguments); this.option.elements = elements; }, /** * @override */ optionUpdated: function (newOption, isInit) { var thisOption = this.option; var newList = (isInit ? thisOption : newOption).elements; var existList = thisOption.elements = isInit ? [] : thisOption.elements; var flattenedList = []; this._flatten(newList, flattenedList); var mappingResult = mappingToExists(existList, flattenedList); makeIdAndName(mappingResult); // Clear elOptionsToUpdate var elOptionsToUpdate = this._elOptionsToUpdate = []; each$1(mappingResult, function (resultItem, index) { var newElOption = resultItem.option; if (__DEV__) { assert$1( isObject$1(newElOption) || resultItem.exist, 'Empty graphic option definition' ); } if (!newElOption) { return; } elOptionsToUpdate.push(newElOption); setKeyInfoToNewElOption(resultItem, newElOption); mergeNewElOptionToExist(existList, index, newElOption); setLayoutInfoToExist(existList[index], newElOption); }, this); // Clean for (var i = existList.length - 1; i >= 0; i--) { if (existList[i] == null) { existList.splice(i, 1); } else { // $action should be volatile, otherwise option gotten from // `getOption` will contain unexpected $action. delete existList[i].$action; } } }, /** * Convert * [{ * type: 'group', * id: 'xx', * children: [{type: 'circle'}, {type: 'polygon'}] * }] * to * [ * {type: 'group', id: 'xx'}, * {type: 'circle', parentId: 'xx'}, * {type: 'polygon', parentId: 'xx'} * ] * * @private * @param {Array.} optionList option list * @param {Array.} result result of flatten * @param {Object} parentOption parent option */ _flatten: function (optionList, result, parentOption) { each$1(optionList, function (option) { if (!option) { return; } if (parentOption) { option.parentOption = parentOption; } result.push(option); var children = option.children; if (option.type === 'group' && children) { this._flatten(children, result, option); } // Deleting for JSON output, and for not affecting group creation. delete option.children; }, this); }, // FIXME // Pass to view using payload? setOption has a payload? useElOptionsToUpdate: function () { var els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming. this._elOptionsToUpdate = null; return els; } }); // ----- // View // ----- extendComponentView({ type: 'graphic', /** * @override */ init: function (ecModel, api) { /** * @private * @type {module:zrender/core/util.HashMap} */ this._elMap = createHashMap(); /** * @private * @type {module:echarts/graphic/GraphicModel} */ this._lastGraphicModel; }, /** * @override */ render: function (graphicModel, ecModel, api) { // Having leveraged between use cases and algorithm complexity, a very // simple layout mechanism is used: // The size(width/height) can be determined by itself or its parent (not // implemented yet), but can not by its children. (Top-down travel) // The location(x/y) can be determined by the bounding rect of itself // (can including its descendants or not) and the size of its parent. // (Bottom-up travel) // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, // view will be reused. if (graphicModel !== this._lastGraphicModel) { this._clear(); } this._lastGraphicModel = graphicModel; this._updateElements(graphicModel); this._relocate(graphicModel, api); }, /** * Update graphic elements. * * @private * @param {Object} graphicModel graphic model */ _updateElements: function (graphicModel) { var elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); if (!elOptionsToUpdate) { return; } var elMap = this._elMap; var rootGroup = this.group; // Top-down tranverse to assign graphic settings to each elements. each$1(elOptionsToUpdate, function (elOption) { var $action = elOption.$action; var id = elOption.id; var existEl = elMap.get(id); var parentId = elOption.parentId; var targetElParent = parentId != null ? elMap.get(parentId) : rootGroup; var elOptionStyle = elOption.style; if (elOption.type === 'text' && elOptionStyle) { // In top/bottom mode, textVerticalAlign should not be used, which cause // inaccurately locating. if (elOption.hv && elOption.hv[1]) { elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null; } // Compatible with previous setting: both support fill and textFill, // stroke and textStroke. !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( elOptionStyle.textFill = elOptionStyle.fill ); !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( elOptionStyle.textStroke = elOptionStyle.stroke ); } // Remove unnecessary props to avoid potential problems. var elOptionCleaned = getCleanedElOption(elOption); // For simple, do not support parent change, otherwise reorder is needed. if (__DEV__) { existEl && assert$1( targetElParent === existEl.parent, 'Changing parent is not supported.' ); } if (!$action || $action === 'merge') { existEl ? existEl.attr(elOptionCleaned) : createEl(id, targetElParent, elOptionCleaned, elMap); } else if ($action === 'replace') { removeEl(existEl, elMap); createEl(id, targetElParent, elOptionCleaned, elMap); } else if ($action === 'remove') { removeEl(existEl, elMap); } var el = elMap.get(id); if (el) { el.__ecGraphicWidthOption = elOption.width; el.__ecGraphicHeightOption = elOption.height; setEventData(el, graphicModel, elOption); } }); }, /** * Locate graphic elements. * * @private * @param {Object} graphicModel graphic model * @param {module:echarts/ExtensionAPI} api extension API */ _relocate: function (graphicModel, api) { var elOptions = graphicModel.option.elements; var rootGroup = this.group; var elMap = this._elMap; var apiWidth = api.getWidth(); var apiHeight = api.getHeight(); // Top-down to calculate percentage width/height of group for (var i = 0; i < elOptions.length; i++) { var elOption = elOptions[i]; var el = elMap.get(elOption.id); if (!el || !el.isGroup) { continue; } var parentEl = el.parent; var isParentRoot = parentEl === rootGroup; // Like 'position:absolut' in css, default 0. el.__ecGraphicWidth = parsePercent$1( el.__ecGraphicWidthOption, isParentRoot ? apiWidth : parentEl.__ecGraphicWidth ) || 0; el.__ecGraphicHeight = parsePercent$1( el.__ecGraphicHeightOption, isParentRoot ? apiHeight : parentEl.__ecGraphicHeight ) || 0; } // Bottom-up tranvese all elements (consider ec resize) to locate elements. for (var i = elOptions.length - 1; i >= 0; i--) { var elOption = elOptions[i]; var el = elMap.get(elOption.id); if (!el) { continue; } var parentEl = el.parent; var containerInfo = parentEl === rootGroup ? { width: apiWidth, height: apiHeight } : { width: parentEl.__ecGraphicWidth, height: parentEl.__ecGraphicHeight }; // PENDING // Currently, when `bounding: 'all'`, the union bounding rect of the group // does not include the rect of [0, 0, group.width, group.height], which // is probably weird for users. Should we make a break change for it? positionElement( el, elOption, containerInfo, null, {hv: elOption.hv, boundingMode: elOption.bounding} ); } }, /** * Clear all elements. * * @private */ _clear: function () { var elMap = this._elMap; elMap.each(function (el) { removeEl(el, elMap); }); this._elMap = createHashMap(); }, /** * @override */ dispose: function () { this._clear(); } }); function createEl(id, targetElParent, elOption, elMap) { var graphicType = elOption.type; if (__DEV__) { assert$1(graphicType, 'graphic type MUST be set'); } var Clz = _nonShapeGraphicElements.hasOwnProperty(graphicType) // Those graphic elements are not shapes. They should not be // overwritten by users, so do them first. ? _nonShapeGraphicElements[graphicType] : getShapeClass(graphicType); if (__DEV__) { assert$1(Clz, 'graphic type can not be found'); } var el = new Clz(elOption); targetElParent.add(el); elMap.set(id, el); el.__ecGraphicId = id; } function removeEl(existEl, elMap) { var existElParent = existEl && existEl.parent; if (existElParent) { existEl.type === 'group' && existEl.traverse(function (el) { removeEl(el, elMap); }); elMap.removeKey(existEl.__ecGraphicId); existElParent.remove(existEl); } } // Remove unnecessary props to avoid potential problems. function getCleanedElOption(elOption) { elOption = extend({}, elOption); each$1( ['id', 'parentId', '$action', 'hv', 'bounding'].concat(LOCATION_PARAMS), function (name) { delete elOption[name]; } ); return elOption; } function isSetLoc(obj, props) { var isSet; each$1(props, function (prop) { obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); }); return isSet; } function setKeyInfoToNewElOption(resultItem, newElOption) { var existElOption = resultItem.exist; // Set id and type after id assigned. newElOption.id = resultItem.keyInfo.id; !newElOption.type && existElOption && (newElOption.type = existElOption.type); // Set parent id if not specified if (newElOption.parentId == null) { var newElParentOption = newElOption.parentOption; if (newElParentOption) { newElOption.parentId = newElParentOption.id; } else if (existElOption) { newElOption.parentId = existElOption.parentId; } } // Clear newElOption.parentOption = null; } function mergeNewElOptionToExist(existList, index, newElOption) { // Update existing options, for `getOption` feature. var newElOptCopy = extend({}, newElOption); var existElOption = existList[index]; var $action = newElOption.$action || 'merge'; if ($action === 'merge') { if (existElOption) { if (__DEV__) { var newType = newElOption.type; assert$1( !newType || existElOption.type === newType, 'Please set $action: "replace" to change `type`' ); } // We can ensure that newElOptCopy and existElOption are not // the same object, so `merge` will not change newElOptCopy. merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize. mergeLayoutParam(existElOption, newElOptCopy, {ignoreSize: true}); // Will be used in render. copyLayoutParams(newElOption, existElOption); } else { existList[index] = newElOptCopy; } } else if ($action === 'replace') { existList[index] = newElOptCopy; } else if ($action === 'remove') { // null will be cleaned later. existElOption && (existList[index] = null); } } function setLayoutInfoToExist(existItem, newElOption) { if (!existItem) { return; } existItem.hv = newElOption.hv = [ // Rigid body, dont care `width`. isSetLoc(newElOption, ['left', 'right']), // Rigid body, dont care `height`. isSetLoc(newElOption, ['top', 'bottom']) ]; // Give default group size. Otherwise layout error may occur. if (existItem.type === 'group') { existItem.width == null && (existItem.width = newElOption.width = 0); existItem.height == null && (existItem.height = newElOption.height = 0); } } function setEventData(el, graphicModel, elOption) { var eventData = el.eventData; // Simple optimize for large amount of elements that no need event. if (!el.silent && !el.ignore && !eventData) { eventData = el.eventData = { componentType: 'graphic', componentIndex: graphicModel.componentIndex, name: el.name }; } // `elOption.info` enables user to mount some info on // elements and use them in event handlers. if (eventData) { eventData.info = el.info; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {Object} finder contains {seriesIndex, dataIndex, dataIndexInside} * @param {module:echarts/model/Global} ecModel * @return {Object} {point: [x, y], el: ...} point Will not be null. */ var findPointFromSeries = function (finder, ecModel) { var point = []; var seriesIndex = finder.seriesIndex; var seriesModel; if (seriesIndex == null || !( seriesModel = ecModel.getSeriesByIndex(seriesIndex) )) { return {point: []}; } var data = seriesModel.getData(); var dataIndex = queryDataIndex(data, finder); if (dataIndex == null || dataIndex < 0 || isArray(dataIndex)) { return {point: []}; } var el = data.getItemGraphicEl(dataIndex); var coordSys = seriesModel.coordinateSystem; if (seriesModel.getTooltipPosition) { point = seriesModel.getTooltipPosition(dataIndex) || []; } else if (coordSys && coordSys.dataToPoint) { point = coordSys.dataToPoint( data.getValues( map(coordSys.dimensions, function (dim) { return data.mapDimension(dim); }), dataIndex, true ) ) || []; } else if (el) { // Use graphic bounding rect var rect = el.getBoundingRect().clone(); rect.applyTransform(el.transform); point = [ rect.x + rect.width / 2, rect.y + rect.height / 2 ]; } return {point: point, el: el}; }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$7 = each$1; var curry$2 = curry; var inner$7 = makeInner(); /** * Basic logic: check all axis, if they do not demand show/highlight, * then hide/downplay them. * * @param {Object} coordSysAxesInfo * @param {Object} payload * @param {string} [payload.currTrigger] 'click' | 'mousemove' | 'leave' * @param {Array.} [payload.x] x and y, which are mandatory, specify a point to * trigger axisPointer and tooltip. * @param {Array.} [payload.y] x and y, which are mandatory, specify a point to * trigger axisPointer and tooltip. * @param {Object} [payload.seriesIndex] finder, optional, restrict target axes. * @param {Object} [payload.dataIndex] finder, restrict target axes. * @param {Object} [payload.axesInfo] finder, restrict target axes. * [{ * axisDim: 'x'|'y'|'angle'|..., * axisIndex: ..., * value: ... * }, ...] * @param {Function} [payload.dispatchAction] * @param {Object} [payload.tooltipOption] * @param {Object|Array.|Function} [payload.position] Tooltip position, * which can be specified in dispatchAction * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @return {Object} content of event obj for echarts.connect. */ var axisTrigger = function (payload, ecModel, api) { var currTrigger = payload.currTrigger; var point = [payload.x, payload.y]; var finder = payload; var dispatchAction = payload.dispatchAction || bind(api.dispatchAction, api); var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo; // Pending // See #6121. But we are not able to reproduce it yet. if (!coordSysAxesInfo) { return; } if (illegalPoint(point)) { // Used in the default behavior of `connection`: use the sample seriesIndex // and dataIndex. And also used in the tooltipView trigger. point = findPointFromSeries({ seriesIndex: finder.seriesIndex, // Do not use dataIndexInside from other ec instance. // FIXME: auto detect it? dataIndex: finder.dataIndex }, ecModel).point; } var isIllegalPoint = illegalPoint(point); // Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}). // Notice: In this case, it is difficult to get the `point` (which is necessary to show // tooltip, so if point is not given, we just use the point found by sample seriesIndex // and dataIndex. var inputAxesInfo = finder.axesInfo; var axesInfo = coordSysAxesInfo.axesInfo; var shouldHide = currTrigger === 'leave' || illegalPoint(point); var outputFinder = {}; var showValueMap = {}; var dataByCoordSys = {list: [], map: {}}; var updaters = { showPointer: curry$2(showPointer, showValueMap), showTooltip: curry$2(showTooltip, dataByCoordSys) }; // Process for triggered axes. each$7(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) { // If a point given, it must be contained by the coordinate system. var coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point); each$7(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) { var axis = axisInfo.axis; var inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo); // If no inputAxesInfo, no axis is restricted. if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) { var val = inputAxisInfo && inputAxisInfo.value; if (val == null && !isIllegalPoint) { val = axis.pointToData(point); } val != null && processOnAxis(axisInfo, val, updaters, false, outputFinder); } }); }); // Process for linked axes. var linkTriggers = {}; each$7(axesInfo, function (tarAxisInfo, tarKey) { var linkGroup = tarAxisInfo.linkGroup; // If axis has been triggered in the previous stage, it should not be triggered by link. if (linkGroup && !showValueMap[tarKey]) { each$7(linkGroup.axesInfo, function (srcAxisInfo, srcKey) { var srcValItem = showValueMap[srcKey]; // If srcValItem exist, source axis is triggered, so link to target axis. if (srcAxisInfo !== tarAxisInfo && srcValItem) { var val = srcValItem.value; linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper( val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo) ))); linkTriggers[tarAxisInfo.key] = val; } }); } }); each$7(linkTriggers, function (val, tarKey) { processOnAxis(axesInfo[tarKey], val, updaters, true, outputFinder); }); updateModelActually(showValueMap, axesInfo, outputFinder); dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction); dispatchHighDownActually(axesInfo, dispatchAction, api); return outputFinder; }; function processOnAxis(axisInfo, newValue, updaters, dontSnap, outputFinder) { var axis = axisInfo.axis; if (axis.scale.isBlank() || !axis.containData(newValue)) { return; } if (!axisInfo.involveSeries) { updaters.showPointer(axisInfo, newValue); return; } // Heavy calculation. So put it after axis.containData checking. var payloadInfo = buildPayloadsBySeries(newValue, axisInfo); var payloadBatch = payloadInfo.payloadBatch; var snapToValue = payloadInfo.snapToValue; // Fill content of event obj for echarts.connect. // By default use the first involved series data as a sample to connect. if (payloadBatch[0] && outputFinder.seriesIndex == null) { extend(outputFinder, payloadBatch[0]); } // If no linkSource input, this process is for collecting link // target, where snap should not be accepted. if (!dontSnap && axisInfo.snap) { if (axis.containData(snapToValue) && snapToValue != null) { newValue = snapToValue; } } updaters.showPointer(axisInfo, newValue, payloadBatch, outputFinder); // Tooltip should always be snapToValue, otherwise there will be // incorrect "axis value ~ series value" mapping displayed in tooltip. updaters.showTooltip(axisInfo, payloadInfo, snapToValue); } function buildPayloadsBySeries(value, axisInfo) { var axis = axisInfo.axis; var dim = axis.dim; var snapToValue = value; var payloadBatch = []; var minDist = Number.MAX_VALUE; var minDiff = -1; each$7(axisInfo.seriesModels, function (series, idx) { var dataDim = series.getData().mapDimension(dim, true); var seriesNestestValue; var dataIndices; if (series.getAxisTooltipData) { var result = series.getAxisTooltipData(dataDim, value, axis); dataIndices = result.dataIndices; seriesNestestValue = result.nestestValue; } else { dataIndices = series.getData().indicesOfNearest( dataDim[0], value, // Add a threshold to avoid find the wrong dataIndex // when data length is not same. // false, axis.type === 'category' ? 0.5 : null ); if (!dataIndices.length) { return; } seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]); } if (seriesNestestValue == null || !isFinite(seriesNestestValue)) { return; } var diff = value - seriesNestestValue; var dist = Math.abs(diff); // Consider category case if (dist <= minDist) { if (dist < minDist || (diff >= 0 && minDiff < 0)) { minDist = dist; minDiff = diff; snapToValue = seriesNestestValue; payloadBatch.length = 0; } each$7(dataIndices, function (dataIndex) { payloadBatch.push({ seriesIndex: series.seriesIndex, dataIndexInside: dataIndex, dataIndex: series.getData().getRawIndex(dataIndex) }); }); } }); return { payloadBatch: payloadBatch, snapToValue: snapToValue }; } function showPointer(showValueMap, axisInfo, value, payloadBatch) { showValueMap[axisInfo.key] = {value: value, payloadBatch: payloadBatch}; } function showTooltip(dataByCoordSys, axisInfo, payloadInfo, value) { var payloadBatch = payloadInfo.payloadBatch; var axis = axisInfo.axis; var axisModel = axis.model; var axisPointerModel = axisInfo.axisPointerModel; // If no data, do not create anything in dataByCoordSys, // whose length will be used to judge whether dispatch action. if (!axisInfo.triggerTooltip || !payloadBatch.length) { return; } var coordSysModel = axisInfo.coordSys.model; var coordSysKey = makeKey(coordSysModel); var coordSysItem = dataByCoordSys.map[coordSysKey]; if (!coordSysItem) { coordSysItem = dataByCoordSys.map[coordSysKey] = { coordSysId: coordSysModel.id, coordSysIndex: coordSysModel.componentIndex, coordSysType: coordSysModel.type, coordSysMainType: coordSysModel.mainType, dataByAxis: [] }; dataByCoordSys.list.push(coordSysItem); } coordSysItem.dataByAxis.push({ axisDim: axis.dim, axisIndex: axisModel.componentIndex, axisType: axisModel.type, axisId: axisModel.id, value: value, // Caustion: viewHelper.getValueLabel is actually on "view stage", which // depends that all models have been updated. So it should not be performed // here. Considering axisPointerModel used here is volatile, which is hard // to be retrieve in TooltipView, we prepare parameters here. valueLabelOpt: { precision: axisPointerModel.get('label.precision'), formatter: axisPointerModel.get('label.formatter') }, seriesDataIndices: payloadBatch.slice() }); } function updateModelActually(showValueMap, axesInfo, outputFinder) { var outputAxesInfo = outputFinder.axesInfo = []; // Basic logic: If no 'show' required, 'hide' this axisPointer. each$7(axesInfo, function (axisInfo, key) { var option = axisInfo.axisPointerModel.option; var valItem = showValueMap[key]; if (valItem) { !axisInfo.useHandle && (option.status = 'show'); option.value = valItem.value; // For label formatter param and highlight. option.seriesDataIndices = (valItem.payloadBatch || []).slice(); } // When always show (e.g., handle used), remain // original value and status. else { // If hide, value still need to be set, consider // click legend to toggle axis blank. !axisInfo.useHandle && (option.status = 'hide'); } // If status is 'hide', should be no info in payload. option.status === 'show' && outputAxesInfo.push({ axisDim: axisInfo.axis.dim, axisIndex: axisInfo.axis.model.componentIndex, value: option.value }); }); } function dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction) { // Basic logic: If no showTip required, hideTip will be dispatched. if (illegalPoint(point) || !dataByCoordSys.list.length) { dispatchAction({type: 'hideTip'}); return; } // In most case only one axis (or event one series is used). It is // convinient to fetch payload.seriesIndex and payload.dataIndex // dirtectly. So put the first seriesIndex and dataIndex of the first // axis on the payload. var sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {}; dispatchAction({ type: 'showTip', escapeConnect: true, x: point[0], y: point[1], tooltipOption: payload.tooltipOption, position: payload.position, dataIndexInside: sampleItem.dataIndexInside, dataIndex: sampleItem.dataIndex, seriesIndex: sampleItem.seriesIndex, dataByCoordSys: dataByCoordSys.list }); } function dispatchHighDownActually(axesInfo, dispatchAction, api) { // FIXME // highlight status modification shoule be a stage of main process? // (Consider confilct (e.g., legend and axisPointer) and setOption) var zr = api.getZr(); var highDownKey = 'axisPointerLastHighlights'; var lastHighlights = inner$7(zr)[highDownKey] || {}; var newHighlights = inner$7(zr)[highDownKey] = {}; // Update highlight/downplay status according to axisPointer model. // Build hash map and remove duplicate incidentally. each$7(axesInfo, function (axisInfo, key) { var option = axisInfo.axisPointerModel.option; option.status === 'show' && each$7(option.seriesDataIndices, function (batchItem) { var key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex; newHighlights[key] = batchItem; }); }); // Diff. var toHighlight = []; var toDownplay = []; each$1(lastHighlights, function (batchItem, key) { !newHighlights[key] && toDownplay.push(batchItem); }); each$1(newHighlights, function (batchItem, key) { !lastHighlights[key] && toHighlight.push(batchItem); }); toDownplay.length && api.dispatchAction({ type: 'downplay', escapeConnect: true, batch: toDownplay }); toHighlight.length && api.dispatchAction({ type: 'highlight', escapeConnect: true, batch: toHighlight }); } function findInputAxisInfo(inputAxesInfo, axisInfo) { for (var i = 0; i < (inputAxesInfo || []).length; i++) { var inputAxisInfo = inputAxesInfo[i]; if (axisInfo.axis.dim === inputAxisInfo.axisDim && axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex ) { return inputAxisInfo; } } } function makeMapperParam(axisInfo) { var axisModel = axisInfo.axis.model; var item = {}; var dim = item.axisDim = axisInfo.axis.dim; item.axisIndex = item[dim + 'AxisIndex'] = axisModel.componentIndex; item.axisName = item[dim + 'AxisName'] = axisModel.name; item.axisId = item[dim + 'AxisId'] = axisModel.id; return item; } function illegalPoint(point) { return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var AxisPointerModel = extendComponentModel({ type: 'axisPointer', coordSysAxesInfo: null, defaultOption: { // 'auto' means that show when triggered by tooltip or handle. show: 'auto', // 'click' | 'mousemove' | 'none' triggerOn: null, // set default in AxisPonterView.js zlevel: 0, z: 50, type: 'line', // 'line' 'shadow' 'cross' 'none'. // axispointer triggered by tootip determine snap automatically, // see `modelHelper`. snap: false, triggerTooltip: true, value: null, status: null, // Init value depends on whether handle is used. // [group0, group1, ...] // Each group can be: { // mapper: function () {}, // singleTooltip: 'multiple', // 'multiple' or 'single' // xAxisId: ..., // yAxisName: ..., // angleAxisIndex: ... // } // mapper: can be ignored. // input: {axisInfo, value} // output: {axisInfo, value} link: [], // Do not set 'auto' here, otherwise global animation: false // will not effect at this axispointer. animation: null, animationDurationUpdate: 200, lineStyle: { color: '#aaa', width: 1, type: 'solid' }, shadowStyle: { color: 'rgba(150,150,150,0.3)' }, label: { show: true, formatter: null, // string | Function precision: 'auto', // Or a number like 0, 1, 2 ... margin: 3, color: '#fff', padding: [5, 7, 5, 7], backgroundColor: 'auto', // default: axis line color borderColor: null, borderWidth: 0, shadowBlur: 3, shadowColor: '#aaa' // Considering applicability, common style should // better not have shadowOffset. // shadowOffsetX: 0, // shadowOffsetY: 2 }, handle: { show: false, /* eslint-disable */ icon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z', // jshint ignore:line /* eslint-enable */ size: 45, // handle margin is from symbol center to axis, which is stable when circular move. margin: 50, // color: '#1b8bbd' // color: '#2f4554' color: '#333', shadowBlur: 3, shadowColor: '#aaa', shadowOffsetX: 0, shadowOffsetY: 2, // For mobile performance throttle: 40 } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$8 = makeInner(); var each$8 = each$1; /** * @param {string} key * @param {module:echarts/ExtensionAPI} api * @param {Function} handler * param: {string} currTrigger * param: {Array.} point */ function register(key, api, handler) { if (env$1.node) { return; } var zr = api.getZr(); inner$8(zr).records || (inner$8(zr).records = {}); initGlobalListeners(zr, api); var record = inner$8(zr).records[key] || (inner$8(zr).records[key] = {}); record.handler = handler; } function initGlobalListeners(zr, api) { if (inner$8(zr).initialized) { return; } inner$8(zr).initialized = true; useHandler('click', curry(doEnter, 'click')); useHandler('mousemove', curry(doEnter, 'mousemove')); // useHandler('mouseout', onLeave); useHandler('globalout', onLeave); function useHandler(eventType, cb) { zr.on(eventType, function (e) { var dis = makeDispatchAction(api); each$8(inner$8(zr).records, function (record) { record && cb(record, e, dis.dispatchAction); }); dispatchTooltipFinally(dis.pendings, api); }); } } function dispatchTooltipFinally(pendings, api) { var showLen = pendings.showTip.length; var hideLen = pendings.hideTip.length; var actuallyPayload; if (showLen) { actuallyPayload = pendings.showTip[showLen - 1]; } else if (hideLen) { actuallyPayload = pendings.hideTip[hideLen - 1]; } if (actuallyPayload) { actuallyPayload.dispatchAction = null; api.dispatchAction(actuallyPayload); } } function onLeave(record, e, dispatchAction) { record.handler('leave', null, dispatchAction); } function doEnter(currTrigger, record, e, dispatchAction) { record.handler(currTrigger, e, dispatchAction); } function makeDispatchAction(api) { var pendings = { showTip: [], hideTip: [] }; // FIXME // better approach? // 'showTip' and 'hideTip' can be triggered by axisPointer and tooltip, // which may be conflict, (axisPointer call showTip but tooltip call hideTip); // So we have to add "final stage" to merge those dispatched actions. var dispatchAction = function (payload) { var pendingList = pendings[payload.type]; if (pendingList) { pendingList.push(payload); } else { payload.dispatchAction = dispatchAction; api.dispatchAction(payload); } }; return { dispatchAction: dispatchAction, pendings: pendings }; } /** * @param {string} key * @param {module:echarts/ExtensionAPI} api */ function unregister(key, api) { if (env$1.node) { return; } var zr = api.getZr(); var record = (inner$8(zr).records || {})[key]; if (record) { inner$8(zr).records[key] = null; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var AxisPointerView = extendComponentView({ type: 'axisPointer', render: function (globalAxisPointerModel, ecModel, api) { var globalTooltipModel = ecModel.getComponent('tooltip'); var triggerOn = globalAxisPointerModel.get('triggerOn') || (globalTooltipModel && globalTooltipModel.get('triggerOn') || 'mousemove|click'); // Register global listener in AxisPointerView to enable // AxisPointerView to be independent to Tooltip. register( 'axisPointer', api, function (currTrigger, e, dispatchAction) { // If 'none', it is not controlled by mouse totally. if (triggerOn !== 'none' && (currTrigger === 'leave' || triggerOn.indexOf(currTrigger) >= 0) ) { dispatchAction({ type: 'updateAxisPointer', currTrigger: currTrigger, x: e && e.offsetX, y: e && e.offsetY }); } } ); }, /** * @override */ remove: function (ecModel, api) { unregister(api.getZr(), 'axisPointer'); AxisPointerView.superApply(this._model, 'remove', arguments); }, /** * @override */ dispose: function (ecModel, api) { unregister('axisPointer', api); AxisPointerView.superApply(this._model, 'dispose', arguments); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var inner$9 = makeInner(); var clone$4 = clone; var bind$1 = bind; /** * Base axis pointer class in 2D. * Implemenents {module:echarts/component/axis/IAxisPointer}. */ function BaseAxisPointer() { } BaseAxisPointer.prototype = { /** * @private */ _group: null, /** * @private */ _lastGraphicKey: null, /** * @private */ _handle: null, /** * @private */ _dragging: false, /** * @private */ _lastValue: null, /** * @private */ _lastStatus: null, /** * @private */ _payloadInfo: null, /** * In px, arbitrary value. Do not set too small, * no animation is ok for most cases. * @protected */ animationThreshold: 15, /** * @implement */ render: function (axisModel, axisPointerModel, api, forceRender) { var value = axisPointerModel.get('value'); var status = axisPointerModel.get('status'); // Bind them to `this`, not in closure, otherwise they will not // be replaced when user calling setOption in not merge mode. this._axisModel = axisModel; this._axisPointerModel = axisPointerModel; this._api = api; // Optimize: `render` will be called repeatly during mouse move. // So it is power consuming if performing `render` each time, // especially on mobile device. if (!forceRender && this._lastValue === value && this._lastStatus === status ) { return; } this._lastValue = value; this._lastStatus = status; var group = this._group; var handle = this._handle; if (!status || status === 'hide') { // Do not clear here, for animation better. group && group.hide(); handle && handle.hide(); return; } group && group.show(); handle && handle.show(); // Otherwise status is 'show' var elOption = {}; this.makeElOption(elOption, value, axisModel, axisPointerModel, api); // Enable change axis pointer type. var graphicKey = elOption.graphicKey; if (graphicKey !== this._lastGraphicKey) { this.clear(api); } this._lastGraphicKey = graphicKey; var moveAnimation = this._moveAnimation = this.determineAnimation(axisModel, axisPointerModel); if (!group) { group = this._group = new Group(); this.createPointerEl(group, elOption, axisModel, axisPointerModel); this.createLabelEl(group, elOption, axisModel, axisPointerModel); api.getZr().add(group); } else { var doUpdateProps = curry(updateProps$1, axisPointerModel, moveAnimation); this.updatePointerEl(group, elOption, doUpdateProps, axisPointerModel); this.updateLabelEl(group, elOption, doUpdateProps, axisPointerModel); } updateMandatoryProps(group, axisPointerModel, true); this._renderHandle(value); }, /** * @implement */ remove: function (api) { this.clear(api); }, /** * @implement */ dispose: function (api) { this.clear(api); }, /** * @protected */ determineAnimation: function (axisModel, axisPointerModel) { var animation = axisPointerModel.get('animation'); var axis = axisModel.axis; var isCategoryAxis = axis.type === 'category'; var useSnap = axisPointerModel.get('snap'); // Value axis without snap always do not snap. if (!useSnap && !isCategoryAxis) { return false; } if (animation === 'auto' || animation == null) { var animationThreshold = this.animationThreshold; if (isCategoryAxis && axis.getBandWidth() > animationThreshold) { return true; } // It is important to auto animation when snap used. Consider if there is // a dataZoom, animation will be disabled when too many points exist, while // it will be enabled for better visual effect when little points exist. if (useSnap) { var seriesDataCount = getAxisInfo(axisModel).seriesDataCount; var axisExtent = axis.getExtent(); // Approximate band width return Math.abs(axisExtent[0] - axisExtent[1]) / seriesDataCount > animationThreshold; } return false; } return animation === true; }, /** * add {pointer, label, graphicKey} to elOption * @protected */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { // Shoule be implemenented by sub-class. }, /** * @protected */ createPointerEl: function (group, elOption, axisModel, axisPointerModel) { var pointerOption = elOption.pointer; if (pointerOption) { var pointerEl = inner$9(group).pointerEl = new graphic[pointerOption.type]( clone$4(elOption.pointer) ); group.add(pointerEl); } }, /** * @protected */ createLabelEl: function (group, elOption, axisModel, axisPointerModel) { if (elOption.label) { var labelEl = inner$9(group).labelEl = new Rect( clone$4(elOption.label) ); group.add(labelEl); updateLabelShowHide(labelEl, axisPointerModel); } }, /** * @protected */ updatePointerEl: function (group, elOption, updateProps$$1) { var pointerEl = inner$9(group).pointerEl; if (pointerEl && elOption.pointer) { pointerEl.setStyle(elOption.pointer.style); updateProps$$1(pointerEl, {shape: elOption.pointer.shape}); } }, /** * @protected */ updateLabelEl: function (group, elOption, updateProps$$1, axisPointerModel) { var labelEl = inner$9(group).labelEl; if (labelEl) { labelEl.setStyle(elOption.label.style); updateProps$$1(labelEl, { // Consider text length change in vertical axis, animation should // be used on shape, otherwise the effect will be weird. shape: elOption.label.shape, position: elOption.label.position }); updateLabelShowHide(labelEl, axisPointerModel); } }, /** * @private */ _renderHandle: function (value) { if (this._dragging || !this.updateHandleTransform) { return; } var axisPointerModel = this._axisPointerModel; var zr = this._api.getZr(); var handle = this._handle; var handleModel = axisPointerModel.getModel('handle'); var status = axisPointerModel.get('status'); if (!handleModel.get('show') || !status || status === 'hide') { handle && zr.remove(handle); this._handle = null; return; } var isInit; if (!this._handle) { isInit = true; handle = this._handle = createIcon( handleModel.get('icon'), { cursor: 'move', draggable: true, onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, onmousedown: bind$1(this._onHandleDragMove, this, 0, 0), drift: bind$1(this._onHandleDragMove, this), ondragend: bind$1(this._onHandleDragEnd, this) } ); zr.add(handle); } updateMandatoryProps(handle, axisPointerModel, false); // update style var includeStyles = [ 'color', 'borderColor', 'borderWidth', 'opacity', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY' ]; handle.setStyle(handleModel.getItemStyle(null, includeStyles)); // update position var handleSize = handleModel.get('size'); if (!isArray(handleSize)) { handleSize = [handleSize, handleSize]; } handle.attr('scale', [handleSize[0] / 2, handleSize[1] / 2]); createOrUpdate( this, '_doDispatchAxisPointer', handleModel.get('throttle') || 0, 'fixRate' ); this._moveHandleToValue(value, isInit); }, /** * @private */ _moveHandleToValue: function (value, isInit) { updateProps$1( this._axisPointerModel, !isInit && this._moveAnimation, this._handle, getHandleTransProps(this.getHandleTransform( value, this._axisModel, this._axisPointerModel )) ); }, /** * @private */ _onHandleDragMove: function (dx, dy) { var handle = this._handle; if (!handle) { return; } this._dragging = true; // Persistent for throttle. var trans = this.updateHandleTransform( getHandleTransProps(handle), [dx, dy], this._axisModel, this._axisPointerModel ); this._payloadInfo = trans; handle.stopAnimation(); handle.attr(getHandleTransProps(trans)); inner$9(handle).lastProp = null; this._doDispatchAxisPointer(); }, /** * Throttled method. * @private */ _doDispatchAxisPointer: function () { var handle = this._handle; if (!handle) { return; } var payloadInfo = this._payloadInfo; var axisModel = this._axisModel; this._api.dispatchAction({ type: 'updateAxisPointer', x: payloadInfo.cursorPoint[0], y: payloadInfo.cursorPoint[1], tooltipOption: payloadInfo.tooltipOption, axesInfo: [{ axisDim: axisModel.axis.dim, axisIndex: axisModel.componentIndex }] }); }, /** * @private */ _onHandleDragEnd: function (moveAnimation) { this._dragging = false; var handle = this._handle; if (!handle) { return; } var value = this._axisPointerModel.get('value'); // Consider snap or categroy axis, handle may be not consistent with // axisPointer. So move handle to align the exact value position when // drag ended. this._moveHandleToValue(value); // For the effect: tooltip will be shown when finger holding on handle // button, and will be hidden after finger left handle button. this._api.dispatchAction({ type: 'hideTip' }); }, /** * Should be implemenented by sub-class if support `handle`. * @protected * @param {number} value * @param {module:echarts/model/Model} axisModel * @param {module:echarts/model/Model} axisPointerModel * @return {Object} {position: [x, y], rotation: 0} */ getHandleTransform: null, /** * * Should be implemenented by sub-class if support `handle`. * @protected * @param {Object} transform {position, rotation} * @param {Array.} delta [dx, dy] * @param {module:echarts/model/Model} axisModel * @param {module:echarts/model/Model} axisPointerModel * @return {Object} {position: [x, y], rotation: 0, cursorPoint: [x, y]} */ updateHandleTransform: null, /** * @private */ clear: function (api) { this._lastValue = null; this._lastStatus = null; var zr = api.getZr(); var group = this._group; var handle = this._handle; if (zr && group) { this._lastGraphicKey = null; group && zr.remove(group); handle && zr.remove(handle); this._group = null; this._handle = null; this._payloadInfo = null; } }, /** * @protected */ doClear: function () { // Implemented by sub-class if necessary. }, /** * @protected * @param {Array.} xy * @param {Array.} wh * @param {number} [xDimIndex=0] or 1 */ buildLabel: function (xy, wh, xDimIndex) { xDimIndex = xDimIndex || 0; return { x: xy[xDimIndex], y: xy[1 - xDimIndex], width: wh[xDimIndex], height: wh[1 - xDimIndex] }; } }; BaseAxisPointer.prototype.constructor = BaseAxisPointer; function updateProps$1(animationModel, moveAnimation, el, props) { // Animation optimize. if (!propsEqual(inner$9(el).lastProp, props)) { inner$9(el).lastProp = props; moveAnimation ? updateProps(el, props, animationModel) : (el.stopAnimation(), el.attr(props)); } } function propsEqual(lastProps, newProps) { if (isObject$1(lastProps) && isObject$1(newProps)) { var equals = true; each$1(newProps, function (item, key) { equals = equals && propsEqual(lastProps[key], item); }); return !!equals; } else { return lastProps === newProps; } } function updateLabelShowHide(labelEl, axisPointerModel) { labelEl[axisPointerModel.get('label.show') ? 'show' : 'hide'](); } function getHandleTransProps(trans) { return { position: trans.position.slice(), rotation: trans.rotation || 0 }; } function updateMandatoryProps(group, axisPointerModel, silent) { var z = axisPointerModel.get('z'); var zlevel = axisPointerModel.get('zlevel'); group && group.traverse(function (el) { if (el.type !== 'group') { z != null && (el.z = z); zlevel != null && (el.zlevel = zlevel); el.silent = silent; } }); } enableClassExtend(BaseAxisPointer); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @param {module:echarts/model/Model} axisPointerModel */ function buildElStyle(axisPointerModel) { var axisPointerType = axisPointerModel.get('type'); var styleModel = axisPointerModel.getModel(axisPointerType + 'Style'); var style; if (axisPointerType === 'line') { style = styleModel.getLineStyle(); style.fill = null; } else if (axisPointerType === 'shadow') { style = styleModel.getAreaStyle(); style.stroke = null; } return style; } /** * @param {Function} labelPos {align, verticalAlign, position} */ function buildLabelElOption( elOption, axisModel, axisPointerModel, api, labelPos ) { var value = axisPointerModel.get('value'); var text = getValueLabel( value, axisModel.axis, axisModel.ecModel, axisPointerModel.get('seriesDataIndices'), { precision: axisPointerModel.get('label.precision'), formatter: axisPointerModel.get('label.formatter') } ); var labelModel = axisPointerModel.getModel('label'); var paddings = normalizeCssArray$1(labelModel.get('padding') || 0); var font = labelModel.getFont(); var textRect = getBoundingRect(text, font); var position = labelPos.position; var width = textRect.width + paddings[1] + paddings[3]; var height = textRect.height + paddings[0] + paddings[2]; // Adjust by align. var align = labelPos.align; align === 'right' && (position[0] -= width); align === 'center' && (position[0] -= width / 2); var verticalAlign = labelPos.verticalAlign; verticalAlign === 'bottom' && (position[1] -= height); verticalAlign === 'middle' && (position[1] -= height / 2); // Not overflow ec container confineInContainer(position, width, height, api); var bgColor = labelModel.get('backgroundColor'); if (!bgColor || bgColor === 'auto') { bgColor = axisModel.get('axisLine.lineStyle.color'); } elOption.label = { shape: {x: 0, y: 0, width: width, height: height, r: labelModel.get('borderRadius')}, position: position.slice(), // TODO: rich style: { text: text, textFont: font, textFill: labelModel.getTextColor(), textPosition: 'inside', textPadding: paddings, fill: bgColor, stroke: labelModel.get('borderColor') || 'transparent', lineWidth: labelModel.get('borderWidth') || 0, shadowBlur: labelModel.get('shadowBlur'), shadowColor: labelModel.get('shadowColor'), shadowOffsetX: labelModel.get('shadowOffsetX'), shadowOffsetY: labelModel.get('shadowOffsetY') }, // Lable should be over axisPointer. z2: 10 }; } // Do not overflow ec container function confineInContainer(position, width, height, api) { var viewWidth = api.getWidth(); var viewHeight = api.getHeight(); position[0] = Math.min(position[0] + width, viewWidth) - width; position[1] = Math.min(position[1] + height, viewHeight) - height; position[0] = Math.max(position[0], 0); position[1] = Math.max(position[1], 0); } /** * @param {number} value * @param {module:echarts/coord/Axis} axis * @param {module:echarts/model/Global} ecModel * @param {Object} opt * @param {Array.} seriesDataIndices * @param {number|string} opt.precision 'auto' or a number * @param {string|Function} opt.formatter label formatter */ function getValueLabel(value, axis, ecModel, seriesDataIndices, opt) { value = axis.scale.parse(value); var text = axis.scale.getLabel( // If `precision` is set, width can be fixed (like '12.00500'), which // helps to debounce when when moving label. value, {precision: opt.precision} ); var formatter = opt.formatter; if (formatter) { var params = { value: getAxisRawValue(axis, value), axisDimension: axis.dim, axisIndex: axis.index, seriesData: [] }; each$1(seriesDataIndices, function (idxItem) { var series = ecModel.getSeriesByIndex(idxItem.seriesIndex); var dataIndex = idxItem.dataIndexInside; var dataParams = series && series.getDataParams(dataIndex); dataParams && params.seriesData.push(dataParams); }); if (isString(formatter)) { text = formatter.replace('{value}', text); } else if (isFunction$1(formatter)) { text = formatter(params); } } return text; } /** * @param {module:echarts/coord/Axis} axis * @param {number} value * @param {Object} layoutInfo { * rotation, position, labelOffset, labelDirection, labelMargin * } */ function getTransformedPosition(axis, value, layoutInfo) { var transform = create$1(); rotate(transform, transform, layoutInfo.rotation); translate(transform, transform, layoutInfo.position); return applyTransform$1([ axis.dataToCoord(value), (layoutInfo.labelOffset || 0) + (layoutInfo.labelDirection || 1) * (layoutInfo.labelMargin || 0) ], transform); } function buildCartesianSingleLabelElOption( value, elOption, layoutInfo, axisModel, axisPointerModel, api ) { var textLayout = AxisBuilder.innerTextLayout( layoutInfo.rotation, 0, layoutInfo.labelDirection ); layoutInfo.labelMargin = axisPointerModel.get('label.margin'); buildLabelElOption(elOption, axisModel, axisPointerModel, api, { position: getTransformedPosition(axisModel.axis, value, layoutInfo), align: textLayout.textAlign, verticalAlign: textLayout.textVerticalAlign }); } /** * @param {Array.} p1 * @param {Array.} p2 * @param {number} [xDimIndex=0] or 1 */ function makeLineShape(p1, p2, xDimIndex) { xDimIndex = xDimIndex || 0; return { x1: p1[xDimIndex], y1: p1[1 - xDimIndex], x2: p2[xDimIndex], y2: p2[1 - xDimIndex] }; } /** * @param {Array.} xy * @param {Array.} wh * @param {number} [xDimIndex=0] or 1 */ function makeRectShape(xy, wh, xDimIndex) { xDimIndex = xDimIndex || 0; return { x: xy[xDimIndex], y: xy[1 - xDimIndex], width: wh[xDimIndex], height: wh[1 - xDimIndex] }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var CartesianAxisPointer = BaseAxisPointer.extend({ /** * @override */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { var axis = axisModel.axis; var grid = axis.grid; var axisPointerType = axisPointerModel.get('type'); var otherExtent = getCartesian(grid, axis).getOtherAxis(axis).getGlobalExtent(); var pixelValue = axis.toGlobalCoord(axis.dataToCoord(value, true)); if (axisPointerType && axisPointerType !== 'none') { var elStyle = buildElStyle(axisPointerModel); var pointerOption = pointerShapeBuilder[axisPointerType]( axis, pixelValue, otherExtent ); pointerOption.style = elStyle; elOption.graphicKey = pointerOption.type; elOption.pointer = pointerOption; } var layoutInfo = layout$1(grid.model, axisModel); buildCartesianSingleLabelElOption( value, elOption, layoutInfo, axisModel, axisPointerModel, api ); }, /** * @override */ getHandleTransform: function (value, axisModel, axisPointerModel) { var layoutInfo = layout$1(axisModel.axis.grid.model, axisModel, { labelInside: false }); layoutInfo.labelMargin = axisPointerModel.get('handle.margin'); return { position: getTransformedPosition(axisModel.axis, value, layoutInfo), rotation: layoutInfo.rotation + (layoutInfo.labelDirection < 0 ? Math.PI : 0) }; }, /** * @override */ updateHandleTransform: function (transform, delta, axisModel, axisPointerModel) { var axis = axisModel.axis; var grid = axis.grid; var axisExtent = axis.getGlobalExtent(true); var otherExtent = getCartesian(grid, axis).getOtherAxis(axis).getGlobalExtent(); var dimIndex = axis.dim === 'x' ? 0 : 1; var currPosition = transform.position; currPosition[dimIndex] += delta[dimIndex]; currPosition[dimIndex] = Math.min(axisExtent[1], currPosition[dimIndex]); currPosition[dimIndex] = Math.max(axisExtent[0], currPosition[dimIndex]); var cursorOtherValue = (otherExtent[1] + otherExtent[0]) / 2; var cursorPoint = [cursorOtherValue, cursorOtherValue]; cursorPoint[dimIndex] = currPosition[dimIndex]; // Make tooltip do not overlap axisPointer and in the middle of the grid. var tooltipOptions = [{verticalAlign: 'middle'}, {align: 'center'}]; return { position: currPosition, rotation: transform.rotation, cursorPoint: cursorPoint, tooltipOption: tooltipOptions[dimIndex] }; } }); function getCartesian(grid, axis) { var opt = {}; opt[axis.dim + 'AxisIndex'] = axis.index; return grid.getCartesian(opt); } var pointerShapeBuilder = { line: function (axis, pixelValue, otherExtent) { var targetShape = makeLineShape( [pixelValue, otherExtent[0]], [pixelValue, otherExtent[1]], getAxisDimIndex(axis) ); return { type: 'Line', subPixelOptimize: true, shape: targetShape }; }, shadow: function (axis, pixelValue, otherExtent) { var bandWidth = Math.max(1, axis.getBandWidth()); var span = otherExtent[1] - otherExtent[0]; return { type: 'Rect', shape: makeRectShape( [pixelValue - bandWidth / 2, otherExtent[0]], [bandWidth, span], getAxisDimIndex(axis) ) }; } }; function getAxisDimIndex(axis) { return axis.dim === 'x' ? 0 : 1; } AxisView.registerAxisPointerClass('CartesianAxisPointer', CartesianAxisPointer); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // CartesianAxisPointer is not supposed to be required here. But consider // echarts.simple.js and online build tooltip, which only require gridSimple, // CartesianAxisPointer should be able to required somewhere. registerPreprocessor(function (option) { // Always has a global axisPointerModel for default setting. if (option) { (!option.axisPointer || option.axisPointer.length === 0) && (option.axisPointer = {}); var link = option.axisPointer.link; // Normalize to array to avoid object mergin. But if link // is not set, remain null/undefined, otherwise it will // override existent link setting. if (link && !isArray(link)) { option.axisPointer.link = [link]; } } }); // This process should proformed after coordinate systems created // and series data processed. So put it on statistic processing stage. registerProcessor(PRIORITY.PROCESSOR.STATISTIC, function (ecModel, api) { // Build axisPointerModel, mergin tooltip.axisPointer model for each axis. // allAxesInfo should be updated when setOption performed. ecModel.getComponent('axisPointer').coordSysAxesInfo = collect(ecModel, api); }); // Broadcast to all views. registerAction({ type: 'updateAxisPointer', event: 'updateAxisPointer', update: ':updateAxisPointer' }, axisTrigger); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ extendComponentModel({ type: 'tooltip', dependencies: ['axisPointer'], defaultOption: { zlevel: 0, z: 60, show: true, // tooltip主体内容 showContent: true, // 'trigger' only works on coordinate system. // 'item' | 'axis' | 'none' trigger: 'item', // 'click' | 'mousemove' | 'none' triggerOn: 'mousemove|click', alwaysShowContent: false, displayMode: 'single', // 'single' | 'multipleByCoordSys' renderMode: 'auto', // 'auto' | 'html' | 'richText' // 'auto': use html by default, and use non-html if `document` is not defined // 'html': use html for tooltip // 'richText': use canvas, svg, and etc. for tooltip // 位置 {Array} | {Function} // position: null // Consider triggered from axisPointer handle, verticalAlign should be 'middle' // align: null, // verticalAlign: null, // 是否约束 content 在 viewRect 中。默认 false 是为了兼容以前版本。 confine: false, // 内容格式器:{string}(Template) ¦ {Function} // formatter: null showDelay: 0, // 隐藏延迟,单位ms hideDelay: 100, // 动画变换时间,单位s transitionDuration: 0.4, enterable: false, // 提示背景颜色,默认为透明度为0.7的黑色 backgroundColor: 'rgba(50,50,50,0.7)', // 提示边框颜色 borderColor: '#333', // 提示边框圆角,单位px,默认为4 borderRadius: 4, // 提示边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 提示内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // Extra css text extraCssText: '', // 坐标轴指示器,坐标轴触发有效 axisPointer: { // 默认为直线 // 可选为:'line' | 'shadow' | 'cross' type: 'line', // type 为 line 的时候有效,指定 tooltip line 所在的轴,可选 // 可选 'x' | 'y' | 'angle' | 'radius' | 'auto' // 默认 'auto',会选择类型为 category 的轴,对于双数值轴,笛卡尔坐标系会默认选择 x 轴 // 极坐标系会默认选择 angle 轴 axis: 'auto', animation: 'auto', animationDurationUpdate: 200, animationEasingUpdate: 'exponentialOut', crossStyle: { color: '#999', width: 1, type: 'dashed', // TODO formatter textStyle: {} } // lineStyle and shadowStyle should not be specified here, // otherwise it will always override those styles on option.axisPointer. }, textStyle: { color: '#fff', fontSize: 14 } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$10 = each$1; var toCamelCase$1 = toCamelCase; var vendors = ['', '-webkit-', '-moz-', '-o-']; var gCssText = 'position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;'; /** * @param {number} duration * @return {string} * @inner */ function assembleTransition(duration) { var transitionCurve = 'cubic-bezier(0.23, 1, 0.32, 1)'; var transitionText = 'left ' + duration + 's ' + transitionCurve + ',' + 'top ' + duration + 's ' + transitionCurve; return map(vendors, function (vendorPrefix) { return vendorPrefix + 'transition:' + transitionText; }).join(';'); } /** * @param {Object} textStyle * @return {string} * @inner */ function assembleFont(textStyleModel) { var cssText = []; var fontSize = textStyleModel.get('fontSize'); var color = textStyleModel.getTextColor(); color && cssText.push('color:' + color); cssText.push('font:' + textStyleModel.getFont()); var lineHeight = textStyleModel.get('lineHeight'); if (lineHeight == null) { lineHeight = Math.round(fontSize * 3 / 2); } fontSize && cssText.push('line-height:' + lineHeight + 'px'); var shadowColor = textStyleModel.get('textShadowColor'); var shadowBlur = textStyleModel.get('textShadowBlur') || 0; var shadowOffsetX = textStyleModel.get('textShadowOffsetX') || 0; var shadowOffsetY = textStyleModel.get('textShadowOffsetY') || 0; shadowBlur && cssText.push('text-shadow:' + shadowOffsetX + 'px ' + shadowOffsetY + 'px ' + shadowBlur + 'px ' + shadowColor); each$10(['decoration', 'align'], function (name) { var val = textStyleModel.get(name); val && cssText.push('text-' + name + ':' + val); }); return cssText.join(';'); } /** * @param {Object} tooltipModel * @return {string} * @inner */ function assembleCssText(tooltipModel) { var cssText = []; var transitionDuration = tooltipModel.get('transitionDuration'); var backgroundColor = tooltipModel.get('backgroundColor'); var textStyleModel = tooltipModel.getModel('textStyle'); var padding = tooltipModel.get('padding'); // Animation transition. Do not animate when transitionDuration is 0. transitionDuration && cssText.push(assembleTransition(transitionDuration)); if (backgroundColor) { if (env$1.canvasSupported) { cssText.push('background-Color:' + backgroundColor); } else { // for ie cssText.push( 'background-Color:#' + toHex(backgroundColor) ); cssText.push('filter:alpha(opacity=70)'); } } // Border style each$10(['width', 'color', 'radius'], function (name) { var borderName = 'border-' + name; var camelCase = toCamelCase$1(borderName); var val = tooltipModel.get(camelCase); val != null && cssText.push(borderName + ':' + val + (name === 'color' ? '' : 'px')); }); // Text style cssText.push(assembleFont(textStyleModel)); // Padding if (padding != null) { cssText.push('padding:' + normalizeCssArray$1(padding).join('px ') + 'px'); } return cssText.join(';') + ';'; } // If not able to make, do not modify the input `out`. function makeStyleCoord(out, zr, appendToBody, zrX, zrY) { var zrPainter = zr && zr.painter; if (appendToBody) { var zrViewportRoot = zrPainter && zrPainter.getViewportRoot(); if (zrViewportRoot) { // Some APPs might use scale on body, so we support CSS transform here. transformLocalCoord(out, zrViewportRoot, document.body, zrX, zrY); } } else { out[0] = zrX; out[1] = zrY; // xy should be based on canvas root. But tooltipContent is // the sibling of canvas root. So padding of ec container // should be considered here. var viewportRootOffset = zrPainter && zrPainter.getViewportRootOffset(); if (viewportRootOffset) { out[0] += viewportRootOffset.offsetLeft; out[1] += viewportRootOffset.offsetTop; } } out[2] = out[0] / zr.getWidth(); // The ratio of left to width out[3] = out[1] / zr.getHeight(); // The ratio of top to height } /** * @alias module:echarts/component/tooltip/TooltipContent * @param {HTMLElement} container * @param {ExtensionAPI} api * @param {Object} [opt] * @param {boolean} [opt.appendToBody] * `false`: the DOM element will be inside the container. Default value. * `true`: the DOM element will be appended to HTML body, which avoid * some overflow clip but intrude outside of the container. * @constructor */ function TooltipContent(container, api, opt) { if (env$1.wxa) { return null; } var el = document.createElement('div'); el.domBelongToZr = true; this.el = el; var zr = this._zr = api.getZr(); var appendToBody = this._appendToBody = opt && opt.appendToBody; this._styleCoord = [0, 0, 0, 0]; // [left, top, left/width, top/height] makeStyleCoord(this._styleCoord, zr, appendToBody, api.getWidth() / 2, api.getHeight() / 2); if (appendToBody) { document.body.appendChild(el); } else { container.appendChild(el); } this._container = container; this._show = false; /** * @private */ this._hideTimeout; // FIXME // Is it needed to trigger zr event manually if // the browser do not support `pointer-events: none`. var self = this; el.onmouseenter = function () { // clear the timeout in hideLater and keep showing tooltip if (self._enterable) { clearTimeout(self._hideTimeout); self._show = true; } self._inContent = true; }; el.onmousemove = function (e) { e = e || window.event; if (!self._enterable) { // `pointer-events: none` is set to tooltip content div // if `enterable` is set as `false`, and `el.onmousemove` // can not be triggered. But in browser that do not // support `pointer-events`, we need to do this: // Try trigger zrender event to avoid mouse // in and out shape too frequently var handler = zr.handler; var zrViewportRoot = zr.painter.getViewportRoot(); normalizeEvent(zrViewportRoot, e, true); handler.dispatch('mousemove', e); } }; el.onmouseleave = function () { if (self._enterable) { if (self._show) { self.hideLater(self._hideDelay); } } self._inContent = false; }; } TooltipContent.prototype = { constructor: TooltipContent, /** * @private * @type {boolean} */ _enterable: true, /** * Update when tooltip is rendered */ update: function (tooltipModel) { // FIXME // Move this logic to ec main? var container = this._container; var stl = container.currentStyle || document.defaultView.getComputedStyle(container); var domStyle = container.style; if (domStyle.position !== 'absolute' && stl.position !== 'absolute') { domStyle.position = 'relative'; } var alwaysShowContent = tooltipModel.get('alwaysShowContent'); alwaysShowContent && this._moveTooltipIfResized(); // Hide the tooltip // PENDING // this.hide(); }, /** * when `alwaysShowContent` is true, * we should move the tooltip after chart resized */ _moveTooltipIfResized: function () { var ratioX = this._styleCoord[2]; // The ratio of left to width var ratioY = this._styleCoord[3]; // The ratio of top to height var realX = ratioX * this._zr.getWidth(); var realY = ratioY * this._zr.getHeight(); this.moveTo(realX, realY); }, show: function (tooltipModel) { clearTimeout(this._hideTimeout); var el = this.el; var styleCoord = this._styleCoord; el.style.cssText = gCssText + assembleCssText(tooltipModel) // Because of the reason described in: // http://stackoverflow.com/questions/21125587/css3-transition-not-working-in-chrome-anymore // we should set initial value to `left` and `top`. + ';left:' + styleCoord[0] + 'px;top:' + styleCoord[1] + 'px;' + (tooltipModel.get('extraCssText') || ''); el.style.display = el.innerHTML ? 'block' : 'none'; // If mouse occasionally move over the tooltip, a mouseout event will be // triggered by canvas, and cause some unexpectable result like dragging // stop, "unfocusAdjacency". Here `pointer-events: none` is used to solve // it. Although it is not supported by IE8~IE10, fortunately it is a rare // scenario. el.style.pointerEvents = this._enterable ? 'auto' : 'none'; this._show = true; }, setContent: function (content) { this.el.innerHTML = content == null ? '' : content; }, setEnterable: function (enterable) { this._enterable = enterable; }, getSize: function () { var el = this.el; return [el.clientWidth, el.clientHeight]; }, moveTo: function (zrX, zrY) { var styleCoord = this._styleCoord; makeStyleCoord(styleCoord, this._zr, this._appendToBody, zrX, zrY); var style = this.el.style; style.left = styleCoord[0] + 'px'; style.top = styleCoord[1] + 'px'; }, hide: function () { this.el.style.display = 'none'; this._show = false; }, hideLater: function (time) { if (this._show && !(this._inContent && this._enterable)) { if (time) { this._hideDelay = time; // Set show false to avoid invoke hideLater multiple times this._show = false; this._hideTimeout = setTimeout(bind(this.hide, this), time); } else { this.hide(); } } }, isShow: function () { return this._show; }, dispose: function () { this.el.parentNode.removeChild(this.el); }, getOuterSize: function () { var width = this.el.clientWidth; var height = this.el.clientHeight; // Consider browser compatibility. // IE8 does not support getComputedStyle. if (document.defaultView && document.defaultView.getComputedStyle) { var stl = document.defaultView.getComputedStyle(this.el); if (stl) { width += parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10); height += parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10); } } return {width: width, height: height}; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import Group from 'zrender/src/container/Group'; function makeStyleCoord$1(out, zr, zrX, zrY) { out[0] = zrX; out[1] = zrY; out[2] = out[0] / zr.getWidth(); // The ratio of left to width out[3] = out[1] / zr.getHeight(); // The ratio of top to height } /** * @alias module:echarts/component/tooltip/TooltipRichContent * @constructor */ function TooltipRichContent(api) { var zr = this._zr = api.getZr(); this._styleCoord = [0, 0, 0, 0]; // [left, top, left/width, top/height] makeStyleCoord$1(this._styleCoord, zr, api.getWidth() / 2, api.getHeight() / 2); this._show = false; /** * @private */ this._hideTimeout; } TooltipRichContent.prototype = { constructor: TooltipRichContent, /** * @private * @type {boolean} */ _enterable: true, /** * Update when tooltip is rendered */ update: function (tooltipModel) { var alwaysShowContent = tooltipModel.get('alwaysShowContent'); alwaysShowContent && this._moveTooltipIfResized(); }, /** * when `alwaysShowContent` is true, * we should move the tooltip after chart resized */ _moveTooltipIfResized: function () { var ratioX = this._styleCoord[2]; // The ratio of left to width var ratioY = this._styleCoord[3]; // The ratio of top to height var realX = ratioX * this._zr.getWidth(); var realY = ratioY * this._zr.getHeight(); this.moveTo(realX, realY); }, show: function (tooltipModel) { if (this._hideTimeout) { clearTimeout(this._hideTimeout); } this.el.attr('show', true); this._show = true; }, /** * Set tooltip content * * @param {string} content rich text string of content * @param {Object} markerRich rich text style * @param {Object} tooltipModel tooltip model */ setContent: function (content, markerRich, tooltipModel) { if (this.el) { this._zr.remove(this.el); } var markers = {}; var text = content; var prefix = '{marker'; var suffix = '|}'; var startId = text.indexOf(prefix); while (startId >= 0) { var endId = text.indexOf(suffix); var name = text.substr(startId + prefix.length, endId - startId - prefix.length); if (name.indexOf('sub') > -1) { markers['marker' + name] = { textWidth: 4, textHeight: 4, textBorderRadius: 2, textBackgroundColor: markerRich[name], // TODO: textOffset is not implemented for rich text textOffset: [3, 0] }; } else { markers['marker' + name] = { textWidth: 10, textHeight: 10, textBorderRadius: 5, textBackgroundColor: markerRich[name] }; } text = text.substr(endId + 1); startId = text.indexOf('{marker'); } var textStyleModel = tooltipModel.getModel('textStyle'); var fontSize = textStyleModel.get('fontSize'); var lineHeight = tooltipModel.get('textLineHeight'); if (lineHeight == null) { lineHeight = Math.round(fontSize * 3 / 2); } this.el = new Text({ style: setTextStyle({}, textStyleModel, { rich: markers, text: content, textBackgroundColor: tooltipModel.get('backgroundColor'), textBorderRadius: tooltipModel.get('borderRadius'), textFill: tooltipModel.get('textStyle.color'), textPadding: tooltipModel.get('padding'), textLineHeight: lineHeight }), z: tooltipModel.get('z') }); this._zr.add(this.el); var self = this; this.el.on('mouseover', function () { // clear the timeout in hideLater and keep showing tooltip if (self._enterable) { clearTimeout(self._hideTimeout); self._show = true; } self._inContent = true; }); this.el.on('mouseout', function () { if (self._enterable) { if (self._show) { self.hideLater(self._hideDelay); } } self._inContent = false; }); }, setEnterable: function (enterable) { this._enterable = enterable; }, getSize: function () { var bounding = this.el.getBoundingRect(); return [bounding.width, bounding.height]; }, moveTo: function (x, y) { if (this.el) { var styleCoord = this._styleCoord; makeStyleCoord$1(styleCoord, this._zr, x, y); this.el.attr('position', [styleCoord[0], styleCoord[1]]); } }, hide: function () { if (this.el) { this.el.hide(); } this._show = false; }, hideLater: function (time) { if (this._show && !(this._inContent && this._enterable)) { if (time) { this._hideDelay = time; // Set show false to avoid invoke hideLater multiple times this._show = false; this._hideTimeout = setTimeout(bind(this.hide, this), time); } else { this.hide(); } } }, isShow: function () { return this._show; }, dispose: function () { clearTimeout(this._hideTimeout); if (this.el) { this._zr.remove(this.el); } }, getOuterSize: function () { var size = this.getSize(); return { width: size[0], height: size[1] }; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var bind$2 = bind; var each$9 = each$1; var parsePercent$2 = parsePercent$1; var proxyRect = new Rect({ shape: {x: -1, y: -1, width: 2, height: 2} }); extendComponentView({ type: 'tooltip', init: function (ecModel, api) { if (env$1.node) { return; } var tooltipModel = ecModel.getComponent('tooltip'); var renderMode = tooltipModel.get('renderMode'); this._renderMode = getTooltipRenderMode(renderMode); var tooltipContent; if (this._renderMode === 'html') { tooltipContent = new TooltipContent(api.getDom(), api, { appendToBody: tooltipModel.get('appendToBody', true) }); this._newLine = '
'; } else { tooltipContent = new TooltipRichContent(api); this._newLine = '\n'; } this._tooltipContent = tooltipContent; }, render: function (tooltipModel, ecModel, api) { if (env$1.node) { return; } // Reset this.group.removeAll(); /** * @private * @type {module:echarts/component/tooltip/TooltipModel} */ this._tooltipModel = tooltipModel; /** * @private * @type {module:echarts/model/Global} */ this._ecModel = ecModel; /** * @private * @type {module:echarts/ExtensionAPI} */ this._api = api; /** * Should be cleaned when render. * @private * @type {Array.>} */ this._lastDataByCoordSys = null; /** * @private * @type {boolean} */ this._alwaysShowContent = tooltipModel.get('alwaysShowContent'); var tooltipContent = this._tooltipContent; tooltipContent.update(tooltipModel); tooltipContent.setEnterable(tooltipModel.get('enterable')); this._initGlobalListener(); this._keepShow(); }, _initGlobalListener: function () { var tooltipModel = this._tooltipModel; var triggerOn = tooltipModel.get('triggerOn'); register( 'itemTooltip', this._api, bind$2(function (currTrigger, e, dispatchAction) { // If 'none', it is not controlled by mouse totally. if (triggerOn !== 'none') { if (triggerOn.indexOf(currTrigger) >= 0) { this._tryShow(e, dispatchAction); } else if (currTrigger === 'leave') { this._hide(dispatchAction); } } }, this) ); }, _keepShow: function () { var tooltipModel = this._tooltipModel; var ecModel = this._ecModel; var api = this._api; // Try to keep the tooltip show when refreshing if (this._lastX != null && this._lastY != null // When user is willing to control tooltip totally using API, // self.manuallyShowTip({x, y}) might cause tooltip hide, // which is not expected. && tooltipModel.get('triggerOn') !== 'none' ) { var self = this; clearTimeout(this._refreshUpdateTimeout); this._refreshUpdateTimeout = setTimeout(function () { // Show tip next tick after other charts are rendered // In case highlight action has wrong result // FIXME !api.isDisposed() && self.manuallyShowTip(tooltipModel, ecModel, api, { x: self._lastX, y: self._lastY }); }); } }, /** * Show tip manually by * dispatchAction({ * type: 'showTip', * x: 10, * y: 10 * }); * Or * dispatchAction({ * type: 'showTip', * seriesIndex: 0, * dataIndex or dataIndexInside or name * }); * * TODO Batch */ manuallyShowTip: function (tooltipModel, ecModel, api, payload) { if (payload.from === this.uid || env$1.node) { return; } var dispatchAction = makeDispatchAction$1(payload, api); // Reset ticket this._ticket = ''; // When triggered from axisPointer. var dataByCoordSys = payload.dataByCoordSys; if (payload.tooltip && payload.x != null && payload.y != null) { var el = proxyRect; el.position = [payload.x, payload.y]; el.update(); el.tooltip = payload.tooltip; // Manually show tooltip while view is not using zrender elements. this._tryShow({ offsetX: payload.x, offsetY: payload.y, target: el }, dispatchAction); } else if (dataByCoordSys) { this._tryShow({ offsetX: payload.x, offsetY: payload.y, position: payload.position, dataByCoordSys: payload.dataByCoordSys, tooltipOption: payload.tooltipOption }, dispatchAction); } else if (payload.seriesIndex != null) { if (this._manuallyAxisShowTip(tooltipModel, ecModel, api, payload)) { return; } var pointInfo = findPointFromSeries(payload, ecModel); var cx = pointInfo.point[0]; var cy = pointInfo.point[1]; if (cx != null && cy != null) { this._tryShow({ offsetX: cx, offsetY: cy, position: payload.position, target: pointInfo.el }, dispatchAction); } } else if (payload.x != null && payload.y != null) { // FIXME // should wrap dispatchAction like `axisPointer/globalListener` ? api.dispatchAction({ type: 'updateAxisPointer', x: payload.x, y: payload.y }); this._tryShow({ offsetX: payload.x, offsetY: payload.y, position: payload.position, target: api.getZr().findHover(payload.x, payload.y).target }, dispatchAction); } }, manuallyHideTip: function (tooltipModel, ecModel, api, payload) { var tooltipContent = this._tooltipContent; if (!this._alwaysShowContent && this._tooltipModel) { tooltipContent.hideLater(this._tooltipModel.get('hideDelay')); } this._lastX = this._lastY = null; if (payload.from !== this.uid) { this._hide(makeDispatchAction$1(payload, api)); } }, // Be compatible with previous design, that is, when tooltip.type is 'axis' and // dispatchAction 'showTip' with seriesIndex and dataIndex will trigger axis pointer // and tooltip. _manuallyAxisShowTip: function (tooltipModel, ecModel, api, payload) { var seriesIndex = payload.seriesIndex; var dataIndex = payload.dataIndex; var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo; if (seriesIndex == null || dataIndex == null || coordSysAxesInfo == null) { return; } var seriesModel = ecModel.getSeriesByIndex(seriesIndex); if (!seriesModel) { return; } var data = seriesModel.getData(); var tooltipModel = buildTooltipModel([ data.getItemModel(dataIndex), seriesModel, (seriesModel.coordinateSystem || {}).model, tooltipModel ]); if (tooltipModel.get('trigger') !== 'axis') { return; } api.dispatchAction({ type: 'updateAxisPointer', seriesIndex: seriesIndex, dataIndex: dataIndex, position: payload.position }); return true; }, _tryShow: function (e, dispatchAction) { var el = e.target; var tooltipModel = this._tooltipModel; if (!tooltipModel) { return; } // Save mouse x, mouse y. So we can try to keep showing the tip if chart is refreshed this._lastX = e.offsetX; this._lastY = e.offsetY; var dataByCoordSys = e.dataByCoordSys; if (dataByCoordSys && dataByCoordSys.length) { this._showAxisTooltip(dataByCoordSys, e); } // Always show item tooltip if mouse is on the element with dataIndex else if (el && el.dataIndex != null) { this._lastDataByCoordSys = null; this._showSeriesItemTooltip(e, el, dispatchAction); } // Tooltip provided directly. Like legend. else if (el && el.tooltip) { this._lastDataByCoordSys = null; this._showComponentItemTooltip(e, el, dispatchAction); } else { this._lastDataByCoordSys = null; this._hide(dispatchAction); } }, _showOrMove: function (tooltipModel, cb) { // showDelay is used in this case: tooltip.enterable is set // as true. User intent to move mouse into tooltip and click // something. `showDelay` makes it easier to enter the content // but tooltip do not move immediately. var delay = tooltipModel.get('showDelay'); cb = bind(cb, this); clearTimeout(this._showTimout); delay > 0 ? (this._showTimout = setTimeout(cb, delay)) : cb(); }, _showAxisTooltip: function (dataByCoordSys, e) { var ecModel = this._ecModel; var globalTooltipModel = this._tooltipModel; var point = [e.offsetX, e.offsetY]; var singleDefaultHTML = []; var singleParamsList = []; var singleTooltipModel = buildTooltipModel([ e.tooltipOption, globalTooltipModel ]); var renderMode = this._renderMode; var newLine = this._newLine; var markers = {}; each$9(dataByCoordSys, function (itemCoordSys) { // var coordParamList = []; // var coordDefaultHTML = []; // var coordTooltipModel = buildTooltipModel([ // e.tooltipOption, // itemCoordSys.tooltipOption, // ecModel.getComponent(itemCoordSys.coordSysMainType, itemCoordSys.coordSysIndex), // globalTooltipModel // ]); // var displayMode = coordTooltipModel.get('displayMode'); // var paramsList = displayMode === 'single' ? singleParamsList : []; each$9(itemCoordSys.dataByAxis, function (item) { var axisModel = ecModel.getComponent(item.axisDim + 'Axis', item.axisIndex); var axisValue = item.value; var seriesDefaultHTML = []; if (!axisModel || axisValue == null) { return; } var valueLabel = getValueLabel( axisValue, axisModel.axis, ecModel, item.seriesDataIndices, item.valueLabelOpt ); each$1(item.seriesDataIndices, function (idxItem) { var series = ecModel.getSeriesByIndex(idxItem.seriesIndex); var dataIndex = idxItem.dataIndexInside; var dataParams = series && series.getDataParams(dataIndex); dataParams.axisDim = item.axisDim; dataParams.axisIndex = item.axisIndex; dataParams.axisType = item.axisType; dataParams.axisId = item.axisId; dataParams.axisValue = getAxisRawValue(axisModel.axis, axisValue); dataParams.axisValueLabel = valueLabel; if (dataParams) { singleParamsList.push(dataParams); var seriesTooltip = series.formatTooltip(dataIndex, true, null, renderMode); var html; if (isObject$1(seriesTooltip)) { html = seriesTooltip.html; var newMarkers = seriesTooltip.markers; merge(markers, newMarkers); } else { html = seriesTooltip; } seriesDefaultHTML.push(html); } }); // Default tooltip content // FIXME // (1) should be the first data which has name? // (2) themeRiver, firstDataIndex is array, and first line is unnecessary. var firstLine = valueLabel; if (renderMode !== 'html') { singleDefaultHTML.push(seriesDefaultHTML.join(newLine)); } else { singleDefaultHTML.push( (firstLine ? encodeHTML(firstLine) + newLine : '') + seriesDefaultHTML.join(newLine) ); } }); }, this); // In most case, the second axis is shown upper than the first one. singleDefaultHTML.reverse(); singleDefaultHTML = singleDefaultHTML.join(this._newLine + this._newLine); var positionExpr = e.position; this._showOrMove(singleTooltipModel, function () { if (this._updateContentNotChangedOnAxis(dataByCoordSys)) { this._updatePosition( singleTooltipModel, positionExpr, point[0], point[1], this._tooltipContent, singleParamsList ); } else { this._showTooltipContent( singleTooltipModel, singleDefaultHTML, singleParamsList, Math.random(), point[0], point[1], positionExpr, undefined, markers ); } }); // Do not trigger events here, because this branch only be entered // from dispatchAction. }, _showSeriesItemTooltip: function (e, el, dispatchAction) { var ecModel = this._ecModel; // Use dataModel in element if possible // Used when mouseover on a element like markPoint or edge // In which case, the data is not main data in series. var seriesIndex = el.seriesIndex; var seriesModel = ecModel.getSeriesByIndex(seriesIndex); // For example, graph link. var dataModel = el.dataModel || seriesModel; var dataIndex = el.dataIndex; var dataType = el.dataType; var data = dataModel.getData(dataType); var tooltipModel = buildTooltipModel([ data.getItemModel(dataIndex), dataModel, seriesModel && (seriesModel.coordinateSystem || {}).model, this._tooltipModel ]); var tooltipTrigger = tooltipModel.get('trigger'); if (tooltipTrigger != null && tooltipTrigger !== 'item') { return; } var params = dataModel.getDataParams(dataIndex, dataType); var seriesTooltip = dataModel.formatTooltip(dataIndex, false, dataType, this._renderMode); var defaultHtml; var markers; if (isObject$1(seriesTooltip)) { defaultHtml = seriesTooltip.html; markers = seriesTooltip.markers; } else { defaultHtml = seriesTooltip; markers = null; } var asyncTicket = 'item_' + dataModel.name + '_' + dataIndex; this._showOrMove(tooltipModel, function () { this._showTooltipContent( tooltipModel, defaultHtml, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target, markers ); }); // FIXME // duplicated showtip if manuallyShowTip is called from dispatchAction. dispatchAction({ type: 'showTip', dataIndexInside: dataIndex, dataIndex: data.getRawIndex(dataIndex), seriesIndex: seriesIndex, from: this.uid }); }, _showComponentItemTooltip: function (e, el, dispatchAction) { var tooltipOpt = el.tooltip; if (typeof tooltipOpt === 'string') { var content = tooltipOpt; tooltipOpt = { content: content, // Fixed formatter formatter: content }; } var subTooltipModel = new Model(tooltipOpt, this._tooltipModel, this._ecModel); var defaultHtml = subTooltipModel.get('content'); var asyncTicket = Math.random(); // Do not check whether `trigger` is 'none' here, because `trigger` // only works on coordinate system. In fact, we have not found case // that requires setting `trigger` nothing on component yet. this._showOrMove(subTooltipModel, function () { this._showTooltipContent( subTooltipModel, defaultHtml, subTooltipModel.get('formatterParams') || {}, asyncTicket, e.offsetX, e.offsetY, e.position, el ); }); // If not dispatch showTip, tip may be hide triggered by axis. dispatchAction({ type: 'showTip', from: this.uid }); }, _showTooltipContent: function ( tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el, markers ) { // Reset ticket this._ticket = ''; if (!tooltipModel.get('showContent') || !tooltipModel.get('show')) { return; } var tooltipContent = this._tooltipContent; var formatter = tooltipModel.get('formatter'); positionExpr = positionExpr || tooltipModel.get('position'); var html = defaultHtml; if (formatter && typeof formatter === 'string') { html = formatTpl(formatter, params, true); } else if (typeof formatter === 'function') { var callback = bind$2(function (cbTicket, html) { if (cbTicket === this._ticket) { tooltipContent.setContent(html, markers, tooltipModel); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); } }, this); this._ticket = asyncTicket; html = formatter(params, asyncTicket, callback); } tooltipContent.setContent(html, markers, tooltipModel); tooltipContent.show(tooltipModel); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); }, /** * @param {string|Function|Array.|Object} positionExpr * @param {number} x Mouse x * @param {number} y Mouse y * @param {boolean} confine Whether confine tooltip content in view rect. * @param {Object|} params * @param {module:zrender/Element} el target element * @param {module:echarts/ExtensionAPI} api * @return {Array.} */ _updatePosition: function (tooltipModel, positionExpr, x, y, content, params, el) { var viewWidth = this._api.getWidth(); var viewHeight = this._api.getHeight(); positionExpr = positionExpr || tooltipModel.get('position'); var contentSize = content.getSize(); var align = tooltipModel.get('align'); var vAlign = tooltipModel.get('verticalAlign'); var rect = el && el.getBoundingRect().clone(); el && rect.applyTransform(el.transform); if (typeof positionExpr === 'function') { // Callback of position can be an array or a string specify the position positionExpr = positionExpr([x, y], params, content.el, rect, { viewSize: [viewWidth, viewHeight], contentSize: contentSize.slice() }); } if (isArray(positionExpr)) { x = parsePercent$2(positionExpr[0], viewWidth); y = parsePercent$2(positionExpr[1], viewHeight); } else if (isObject$1(positionExpr)) { positionExpr.width = contentSize[0]; positionExpr.height = contentSize[1]; var layoutRect = getLayoutRect( positionExpr, {width: viewWidth, height: viewHeight} ); x = layoutRect.x; y = layoutRect.y; align = null; // When positionExpr is left/top/right/bottom, // align and verticalAlign will not work. vAlign = null; } // Specify tooltip position by string 'top' 'bottom' 'left' 'right' around graphic element else if (typeof positionExpr === 'string' && el) { var pos = calcTooltipPosition( positionExpr, rect, contentSize ); x = pos[0]; y = pos[1]; } else { var pos = refixTooltipPosition( x, y, content, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20 ); x = pos[0]; y = pos[1]; } align && (x -= isCenterAlign(align) ? contentSize[0] / 2 : align === 'right' ? contentSize[0] : 0); vAlign && (y -= isCenterAlign(vAlign) ? contentSize[1] / 2 : vAlign === 'bottom' ? contentSize[1] : 0); if (tooltipModel.get('confine')) { var pos = confineTooltipPosition( x, y, content, viewWidth, viewHeight ); x = pos[0]; y = pos[1]; } content.moveTo(x, y); }, // FIXME // Should we remove this but leave this to user? _updateContentNotChangedOnAxis: function (dataByCoordSys) { var lastCoordSys = this._lastDataByCoordSys; var contentNotChanged = !!lastCoordSys && lastCoordSys.length === dataByCoordSys.length; contentNotChanged && each$9(lastCoordSys, function (lastItemCoordSys, indexCoordSys) { var lastDataByAxis = lastItemCoordSys.dataByAxis || {}; var thisItemCoordSys = dataByCoordSys[indexCoordSys] || {}; var thisDataByAxis = thisItemCoordSys.dataByAxis || []; contentNotChanged &= lastDataByAxis.length === thisDataByAxis.length; contentNotChanged && each$9(lastDataByAxis, function (lastItem, indexAxis) { var thisItem = thisDataByAxis[indexAxis] || {}; var lastIndices = lastItem.seriesDataIndices || []; var newIndices = thisItem.seriesDataIndices || []; contentNotChanged &= lastItem.value === thisItem.value && lastItem.axisType === thisItem.axisType && lastItem.axisId === thisItem.axisId && lastIndices.length === newIndices.length; contentNotChanged && each$9(lastIndices, function (lastIdxItem, j) { var newIdxItem = newIndices[j]; contentNotChanged &= lastIdxItem.seriesIndex === newIdxItem.seriesIndex && lastIdxItem.dataIndex === newIdxItem.dataIndex; }); }); }); this._lastDataByCoordSys = dataByCoordSys; return !!contentNotChanged; }, _hide: function (dispatchAction) { // Do not directly hideLater here, because this behavior may be prevented // in dispatchAction when showTip is dispatched. // FIXME // duplicated hideTip if manuallyHideTip is called from dispatchAction. this._lastDataByCoordSys = null; dispatchAction({ type: 'hideTip', from: this.uid }); }, dispose: function (ecModel, api) { if (env$1.node) { return; } this._tooltipContent.dispose(); unregister('itemTooltip', api); } }); /** * @param {Array.} modelCascade * From top to bottom. (the last one should be globalTooltipModel); */ function buildTooltipModel(modelCascade) { var resultModel = modelCascade.pop(); while (modelCascade.length) { var tooltipOpt = modelCascade.pop(); if (tooltipOpt) { if (Model.isInstance(tooltipOpt)) { tooltipOpt = tooltipOpt.get('tooltip', true); } // In each data item tooltip can be simply write: // { // value: 10, // tooltip: 'Something you need to know' // } if (typeof tooltipOpt === 'string') { tooltipOpt = {formatter: tooltipOpt}; } resultModel = new Model(tooltipOpt, resultModel, resultModel.ecModel); } } return resultModel; } function makeDispatchAction$1(payload, api) { return payload.dispatchAction || bind(api.dispatchAction, api); } function refixTooltipPosition(x, y, content, viewWidth, viewHeight, gapH, gapV) { var size = content.getOuterSize(); var width = size.width; var height = size.height; if (gapH != null) { if (x + width + gapH > viewWidth) { x -= width + gapH; } else { x += gapH; } } if (gapV != null) { if (y + height + gapV > viewHeight) { y -= height + gapV; } else { y += gapV; } } return [x, y]; } function confineTooltipPosition(x, y, content, viewWidth, viewHeight) { var size = content.getOuterSize(); var width = size.width; var height = size.height; x = Math.min(x + width, viewWidth) - width; y = Math.min(y + height, viewHeight) - height; x = Math.max(x, 0); y = Math.max(y, 0); return [x, y]; } function calcTooltipPosition(position, rect, contentSize) { var domWidth = contentSize[0]; var domHeight = contentSize[1]; var gap = 5; var x = 0; var y = 0; var rectWidth = rect.width; var rectHeight = rect.height; switch (position) { case 'inside': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y + rectHeight / 2 - domHeight / 2; break; case 'top': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y - domHeight - gap; break; case 'bottom': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y + rectHeight + gap; break; case 'left': x = rect.x - domWidth - gap; y = rect.y + rectHeight / 2 - domHeight / 2; break; case 'right': x = rect.x + rectWidth + gap; y = rect.y + rectHeight / 2 - domHeight / 2; } return [x, y]; } function isCenterAlign(align) { return align === 'center' || align === 'middle'; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // FIXME Better way to pack data in graphic element /** * @action * @property {string} type * @property {number} seriesIndex * @property {number} dataIndex * @property {number} [x] * @property {number} [y] */ registerAction( { type: 'showTip', event: 'showTip', update: 'tooltip:manuallyShowTip' }, // noop function () {} ); registerAction( { type: 'hideTip', event: 'hideTip', update: 'tooltip:manuallyHideTip' }, // noop function () {} ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var langSelector = lang.legend.selector; var defaultSelectorOption = { all: { type: 'all', title: clone(langSelector.all) }, inverse: { type: 'inverse', title: clone(langSelector.inverse) } }; var LegendModel = extendComponentModel({ type: 'legend.plain', dependencies: ['series'], layoutMode: { type: 'box', // legend.width/height are maxWidth/maxHeight actually, // whereas realy width/height is calculated by its content. // (Setting {left: 10, right: 10} does not make sense). // So consider the case: // `setOption({legend: {left: 10});` // then `setOption({legend: {right: 10});` // The previous `left` should be cleared by setting `ignoreSize`. ignoreSize: true }, init: function (option, parentModel, ecModel) { this.mergeDefaultAndTheme(option, ecModel); option.selected = option.selected || {}; this._updateSelector(option); }, mergeOption: function (option) { LegendModel.superCall(this, 'mergeOption', option); this._updateSelector(option); }, _updateSelector: function (option) { var selector = option.selector; if (selector === true) { selector = option.selector = ['all', 'inverse']; } if (isArray(selector)) { each$1(selector, function (item, index) { isString(item) && (item = {type: item}); selector[index] = merge(item, defaultSelectorOption[item.type]); }); } }, optionUpdated: function () { this._updateData(this.ecModel); var legendData = this._data; // If selectedMode is single, try to select one if (legendData[0] && this.get('selectedMode') === 'single') { var hasSelected = false; // If has any selected in option.selected for (var i = 0; i < legendData.length; i++) { var name = legendData[i].get('name'); if (this.isSelected(name)) { // Force to unselect others this.select(name); hasSelected = true; break; } } // Try select the first if selectedMode is single !hasSelected && this.select(legendData[0].get('name')); } }, _updateData: function (ecModel) { var potentialData = []; var availableNames = []; ecModel.eachRawSeries(function (seriesModel) { var seriesName = seriesModel.name; availableNames.push(seriesName); var isPotential; if (seriesModel.legendVisualProvider) { var provider = seriesModel.legendVisualProvider; var names = provider.getAllNames(); if (!ecModel.isSeriesFiltered(seriesModel)) { availableNames = availableNames.concat(names); } if (names.length) { potentialData = potentialData.concat(names); } else { isPotential = true; } } else { isPotential = true; } if (isPotential && isNameSpecified(seriesModel)) { potentialData.push(seriesModel.name); } }); /** * @type {Array.} * @private */ this._availableNames = availableNames; // If legend.data not specified in option, use availableNames as data, // which is convinient for user preparing option. var rawData = this.get('data') || potentialData; var legendData = map(rawData, function (dataItem) { // Can be string or number if (typeof dataItem === 'string' || typeof dataItem === 'number') { dataItem = { name: dataItem }; } return new Model(dataItem, this, this.ecModel); }, this); /** * @type {Array.} * @private */ this._data = legendData; }, /** * @return {Array.} */ getData: function () { return this._data; }, /** * @param {string} name */ select: function (name) { var selected = this.option.selected; var selectedMode = this.get('selectedMode'); if (selectedMode === 'single') { var data = this._data; each$1(data, function (dataItem) { selected[dataItem.get('name')] = false; }); } selected[name] = true; }, /** * @param {string} name */ unSelect: function (name) { if (this.get('selectedMode') !== 'single') { this.option.selected[name] = false; } }, /** * @param {string} name */ toggleSelected: function (name) { var selected = this.option.selected; // Default is true if (!selected.hasOwnProperty(name)) { selected[name] = true; } this[selected[name] ? 'unSelect' : 'select'](name); }, allSelect: function () { var data = this._data; var selected = this.option.selected; each$1(data, function (dataItem) { selected[dataItem.get('name', true)] = true; }); }, inverseSelect: function () { var data = this._data; var selected = this.option.selected; each$1(data, function (dataItem) { var name = dataItem.get('name', true); // Initially, default value is true if (!selected.hasOwnProperty(name)) { selected[name] = true; } selected[name] = !selected[name]; }); }, /** * @param {string} name */ isSelected: function (name) { var selected = this.option.selected; return !(selected.hasOwnProperty(name) && !selected[name]) && indexOf(this._availableNames, name) >= 0; }, getOrient: function () { return this.get('orient') === 'vertical' ? {index: 1, name: 'vertical'} : {index: 0, name: 'horizontal'}; }, defaultOption: { // 一级层叠 zlevel: 0, // 二级层叠 z: 4, show: true, // 布局方式,默认为水平布局,可选为: // 'horizontal' | 'vertical' orient: 'horizontal', left: 'center', // right: 'center', top: 0, // bottom: null, // 水平对齐 // 'auto' | 'left' | 'right' // 默认为 'auto', 根据 x 的位置判断是左对齐还是右对齐 align: 'auto', backgroundColor: 'rgba(0,0,0,0)', // 图例边框颜色 borderColor: '#ccc', borderRadius: 0, // 图例边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 图例内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // 各个item之间的间隔,单位px,默认为10, // 横向布局时为水平间隔,纵向布局时为纵向间隔 itemGap: 10, // the width of legend symbol itemWidth: 25, // the height of legend symbol itemHeight: 14, // the color of unselected legend symbol inactiveColor: '#ccc', // the borderColor of unselected legend symbol inactiveBorderColor: '#ccc', itemStyle: { // the default borderWidth of legend symbol borderWidth: 0 }, textStyle: { // 图例文字颜色 color: '#333' }, // formatter: '', // 选择模式,默认开启图例开关 selectedMode: true, // 配置默认选中状态,可配合LEGEND.SELECTED事件做动态数据载入 // selected: null, // 图例内容(详见legend.data,数组中每一项代表一个item // data: [], // Usage: // selector: [{type: 'all or inverse', title: xxx}] // or // selector: true // or // selector: ['all', 'inverse'] selector: false, selectorLabel: { show: true, borderRadius: 10, padding: [3, 5, 3, 5], fontSize: 12, fontFamily: ' sans-serif', color: '#666', borderWidth: 1, borderColor: '#666' }, emphasis: { selectorLabel: { show: true, color: '#eee', backgroundColor: '#666' } }, // Value can be 'start' or 'end' selectorPosition: 'auto', selectorItemGap: 7, selectorButtonGap: 10, // Tooltip 相关配置 tooltip: { show: false } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function legendSelectActionHandler(methodName, payload, ecModel) { var selectedMap = {}; var isToggleSelect = methodName === 'toggleSelected'; var isSelected; // Update all legend components ecModel.eachComponent('legend', function (legendModel) { if (isToggleSelect && isSelected != null) { // Force other legend has same selected status // Or the first is toggled to true and other are toggled to false // In the case one legend has some item unSelected in option. And if other legend // doesn't has the item, they will assume it is selected. legendModel[isSelected ? 'select' : 'unSelect'](payload.name); } else if (methodName === 'allSelect' || methodName === 'inverseSelect') { legendModel[methodName](); } else { legendModel[methodName](payload.name); isSelected = legendModel.isSelected(payload.name); } var legendData = legendModel.getData(); each$1(legendData, function (model) { var name = model.get('name'); // Wrap element if (name === '\n' || name === '') { return; } var isItemSelected = legendModel.isSelected(name); if (selectedMap.hasOwnProperty(name)) { // Unselected if any legend is unselected selectedMap[name] = selectedMap[name] && isItemSelected; } else { selectedMap[name] = isItemSelected; } }); }); // Return the event explicitly return (methodName === 'allSelect' || methodName === 'inverseSelect') ? { selected: selectedMap } : { name: payload.name, selected: selectedMap }; } /** * @event legendToggleSelect * @type {Object} * @property {string} type 'legendToggleSelect' * @property {string} [from] * @property {string} name Series name or data item name */ registerAction( 'legendToggleSelect', 'legendselectchanged', curry(legendSelectActionHandler, 'toggleSelected') ); registerAction( 'legendAllSelect', 'legendselectall', curry(legendSelectActionHandler, 'allSelect') ); registerAction( 'legendInverseSelect', 'legendinverseselect', curry(legendSelectActionHandler, 'inverseSelect') ); /** * @event legendSelect * @type {Object} * @property {string} type 'legendSelect' * @property {string} name Series name or data item name */ registerAction( 'legendSelect', 'legendselected', curry(legendSelectActionHandler, 'select') ); /** * @event legendUnSelect * @type {Object} * @property {string} type 'legendUnSelect' * @property {string} name Series name or data item name */ registerAction( 'legendUnSelect', 'legendunselected', curry(legendSelectActionHandler, 'unSelect') ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Layout list like component. * It will box layout each items in group of component and then position the whole group in the viewport * @param {module:zrender/group/Group} group * @param {module:echarts/model/Component} componentModel * @param {module:echarts/ExtensionAPI} */ function layout$2(group, componentModel, api) { var boxLayoutParams = componentModel.getBoxLayoutParams(); var padding = componentModel.get('padding'); var viewportSize = {width: api.getWidth(), height: api.getHeight()}; var rect = getLayoutRect( boxLayoutParams, viewportSize, padding ); box( componentModel.get('orient'), group, componentModel.get('itemGap'), rect.width, rect.height ); positionElement( group, boxLayoutParams, viewportSize, padding ); } function makeBackground(rect, componentModel) { var padding = normalizeCssArray$1( componentModel.get('padding') ); var style = componentModel.getItemStyle(['color', 'opacity']); style.fill = componentModel.get('backgroundColor'); var rect = new Rect({ shape: { x: rect.x - padding[3], y: rect.y - padding[0], width: rect.width + padding[1] + padding[3], height: rect.height + padding[0] + padding[2], r: componentModel.get('borderRadius') }, style: style, silent: true, z2: -1 }); // FIXME // `subPixelOptimizeRect` may bring some gap between edge of viewpart // and background rect when setting like `left: 0`, `top: 0`. // graphic.subPixelOptimizeRect(rect); return rect; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var curry$3 = curry; var each$11 = each$1; var Group$2 = Group; var LegendView = extendComponentView({ type: 'legend.plain', newlineDisabled: false, /** * @override */ init: function () { /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._contentGroup = new Group$2()); /** * @private * @type {module:zrender/Element} */ this._backgroundEl; /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._selectorGroup = new Group$2()); /** * If first rendering, `contentGroup.position` is [0, 0], which * does not make sense and may cause unexepcted animation if adopted. * @private * @type {boolean} */ this._isFirstRender = true; }, /** * @protected */ getContentGroup: function () { return this._contentGroup; }, /** * @protected */ getSelectorGroup: function () { return this._selectorGroup; }, /** * @override */ render: function (legendModel, ecModel, api) { var isFirstRender = this._isFirstRender; this._isFirstRender = false; this.resetInner(); if (!legendModel.get('show', true)) { return; } var itemAlign = legendModel.get('align'); var orient = legendModel.get('orient'); if (!itemAlign || itemAlign === 'auto') { itemAlign = ( legendModel.get('left') === 'right' && orient === 'vertical' ) ? 'right' : 'left'; } var selector = legendModel.get('selector', true); var selectorPosition = legendModel.get('selectorPosition', true); if (selector && (!selectorPosition || selectorPosition === 'auto')) { selectorPosition = orient === 'horizontal' ? 'end' : 'start'; } this.renderInner(itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition); // Perform layout. var positionInfo = legendModel.getBoxLayoutParams(); var viewportSize = {width: api.getWidth(), height: api.getHeight()}; var padding = legendModel.get('padding'); var maxSize = getLayoutRect(positionInfo, viewportSize, padding); var mainRect = this.layoutInner(legendModel, itemAlign, maxSize, isFirstRender, selector, selectorPosition); // Place mainGroup, based on the calculated `mainRect`. var layoutRect = getLayoutRect( defaults({width: mainRect.width, height: mainRect.height}, positionInfo), viewportSize, padding ); this.group.attr('position', [layoutRect.x - mainRect.x, layoutRect.y - mainRect.y]); // Render background after group is layout. this.group.add( this._backgroundEl = makeBackground(mainRect, legendModel) ); }, /** * @protected */ resetInner: function () { this.getContentGroup().removeAll(); this._backgroundEl && this.group.remove(this._backgroundEl); this.getSelectorGroup().removeAll(); }, /** * @protected */ renderInner: function (itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition) { var contentGroup = this.getContentGroup(); var legendDrawnMap = createHashMap(); var selectMode = legendModel.get('selectedMode'); var excludeSeriesId = []; ecModel.eachRawSeries(function (seriesModel) { !seriesModel.get('legendHoverLink') && excludeSeriesId.push(seriesModel.id); }); each$11(legendModel.getData(), function (itemModel, dataIndex) { var name = itemModel.get('name'); // Use empty string or \n as a newline string if (!this.newlineDisabled && (name === '' || name === '\n')) { contentGroup.add(new Group$2({ newline: true })); return; } // Representitive series. var seriesModel = ecModel.getSeriesByName(name)[0]; if (legendDrawnMap.get(name)) { // Have been drawed return; } // Legend to control series. if (seriesModel) { var data = seriesModel.getData(); var color = data.getVisual('color'); var borderColor = data.getVisual('borderColor'); // If color is a callback function if (typeof color === 'function') { // Use the first data color = color(seriesModel.getDataParams(0)); } // If borderColor is a callback function if (typeof borderColor === 'function') { // Use the first data borderColor = borderColor(seriesModel.getDataParams(0)); } // Using rect symbol defaultly var legendSymbolType = data.getVisual('legendSymbol') || 'roundRect'; var symbolType = data.getVisual('symbol'); var itemGroup = this._createItem( name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, borderColor, selectMode ); itemGroup.on('click', curry$3(dispatchSelectAction, name, null, api, excludeSeriesId)) .on('mouseover', curry$3(dispatchHighlightAction, seriesModel.name, null, api, excludeSeriesId)) .on('mouseout', curry$3(dispatchDownplayAction, seriesModel.name, null, api, excludeSeriesId)); legendDrawnMap.set(name, true); } else { // Legend to control data. In pie and funnel. ecModel.eachRawSeries(function (seriesModel) { // In case multiple series has same data name if (legendDrawnMap.get(name)) { return; } if (seriesModel.legendVisualProvider) { var provider = seriesModel.legendVisualProvider; if (!provider.containName(name)) { return; } var idx = provider.indexOfName(name); var color = provider.getItemVisual(idx, 'color'); var borderColor = provider.getItemVisual(idx, 'borderColor'); var legendSymbolType = 'roundRect'; var itemGroup = this._createItem( name, dataIndex, itemModel, legendModel, legendSymbolType, null, itemAlign, color, borderColor, selectMode ); // FIXME: consider different series has items with the same name. itemGroup.on('click', curry$3(dispatchSelectAction, null, name, api, excludeSeriesId)) // Should not specify the series name, consider legend controls // more than one pie series. .on('mouseover', curry$3(dispatchHighlightAction, null, name, api, excludeSeriesId)) .on('mouseout', curry$3(dispatchDownplayAction, null, name, api, excludeSeriesId)); legendDrawnMap.set(name, true); } }, this); } if (__DEV__) { if (!legendDrawnMap.get(name)) { console.warn( name + ' series not exists. Legend data should be same with series name or data name.' ); } } }, this); if (selector) { this._createSelector(selector, legendModel, api, orient, selectorPosition); } }, _createSelector: function (selector, legendModel, api, orient, selectorPosition) { var selectorGroup = this.getSelectorGroup(); each$11(selector, function (selectorItem) { createSelectorButton(selectorItem); }); function createSelectorButton(selectorItem) { var type = selectorItem.type; var labelText = new Text({ style: { x: 0, y: 0, align: 'center', verticalAlign: 'middle' }, onclick: function () { api.dispatchAction({ type: type === 'all' ? 'legendAllSelect' : 'legendInverseSelect' }); } }); selectorGroup.add(labelText); var labelModel = legendModel.getModel('selectorLabel'); var emphasisLabelModel = legendModel.getModel('emphasis.selectorLabel'); setLabelStyle( labelText.style, labelText.hoverStyle = {}, labelModel, emphasisLabelModel, { defaultText: selectorItem.title, isRectText: false } ); setHoverStyle(labelText); } }, _createItem: function ( name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, borderColor, selectMode ) { var itemWidth = legendModel.get('itemWidth'); var itemHeight = legendModel.get('itemHeight'); var inactiveColor = legendModel.get('inactiveColor'); var inactiveBorderColor = legendModel.get('inactiveBorderColor'); var symbolKeepAspect = legendModel.get('symbolKeepAspect'); var legendModelItemStyle = legendModel.getModel('itemStyle'); var isSelected = legendModel.isSelected(name); var itemGroup = new Group$2(); var textStyleModel = itemModel.getModel('textStyle'); var itemIcon = itemModel.get('icon'); var tooltipModel = itemModel.getModel('tooltip'); var legendGlobalTooltipModel = tooltipModel.parentModel; // Use user given icon first legendSymbolType = itemIcon || legendSymbolType; var legendSymbol = createSymbol( legendSymbolType, 0, 0, itemWidth, itemHeight, isSelected ? color : inactiveColor, // symbolKeepAspect default true for legend symbolKeepAspect == null ? true : symbolKeepAspect ); itemGroup.add( setSymbolStyle( legendSymbol, legendSymbolType, legendModelItemStyle, borderColor, inactiveBorderColor, isSelected ) ); // Compose symbols // PENDING if (!itemIcon && symbolType // At least show one symbol, can't be all none && ((symbolType !== legendSymbolType) || symbolType === 'none') ) { var size = itemHeight * 0.8; if (symbolType === 'none') { symbolType = 'circle'; } var legendSymbolCenter = createSymbol( symbolType, (itemWidth - size) / 2, (itemHeight - size) / 2, size, size, isSelected ? color : inactiveColor, // symbolKeepAspect default true for legend symbolKeepAspect == null ? true : symbolKeepAspect ); // Put symbol in the center itemGroup.add( setSymbolStyle( legendSymbolCenter, symbolType, legendModelItemStyle, borderColor, inactiveBorderColor, isSelected ) ); } var textX = itemAlign === 'left' ? itemWidth + 5 : -5; var textAlign = itemAlign; var formatter = legendModel.get('formatter'); var content = name; if (typeof formatter === 'string' && formatter) { content = formatter.replace('{name}', name != null ? name : ''); } else if (typeof formatter === 'function') { content = formatter(name); } itemGroup.add(new Text({ style: setTextStyle({}, textStyleModel, { text: content, x: textX, y: itemHeight / 2, textFill: isSelected ? textStyleModel.getTextColor() : inactiveColor, textAlign: textAlign, textVerticalAlign: 'middle' }) })); // Add a invisible rect to increase the area of mouse hover var hitRect = new Rect({ shape: itemGroup.getBoundingRect(), invisible: true, tooltip: tooltipModel.get('show') ? extend({ content: name, // Defaul formatter formatter: legendGlobalTooltipModel.get('formatter', true) || function () { return name; }, formatterParams: { componentType: 'legend', legendIndex: legendModel.componentIndex, name: name, $vars: ['name'] } }, tooltipModel.option) : null }); itemGroup.add(hitRect); itemGroup.eachChild(function (child) { child.silent = true; }); hitRect.silent = !selectMode; this.getContentGroup().add(itemGroup); setHoverStyle(itemGroup); itemGroup.__legendDataIndex = dataIndex; return itemGroup; }, /** * @protected */ layoutInner: function (legendModel, itemAlign, maxSize, isFirstRender, selector, selectorPosition) { var contentGroup = this.getContentGroup(); var selectorGroup = this.getSelectorGroup(); // Place items in contentGroup. box( legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), maxSize.width, maxSize.height ); var contentRect = contentGroup.getBoundingRect(); var contentPos = [-contentRect.x, -contentRect.y]; if (selector) { // Place buttons in selectorGroup box( // Buttons in selectorGroup always layout horizontally 'horizontal', selectorGroup, legendModel.get('selectorItemGap', true) ); var selectorRect = selectorGroup.getBoundingRect(); var selectorPos = [-selectorRect.x, -selectorRect.y]; var selectorButtonGap = legendModel.get('selectorButtonGap', true); var orientIdx = legendModel.getOrient().index; var wh = orientIdx === 0 ? 'width' : 'height'; var hw = orientIdx === 0 ? 'height' : 'width'; var yx = orientIdx === 0 ? 'y' : 'x'; if (selectorPosition === 'end') { selectorPos[orientIdx] += contentRect[wh] + selectorButtonGap; } else { contentPos[orientIdx] += selectorRect[wh] + selectorButtonGap; } //Always align selector to content as 'middle' selectorPos[1 - orientIdx] += contentRect[hw] / 2 - selectorRect[hw] / 2; selectorGroup.attr('position', selectorPos); contentGroup.attr('position', contentPos); var mainRect = {x: 0, y: 0}; mainRect[wh] = contentRect[wh] + selectorButtonGap + selectorRect[wh]; mainRect[hw] = Math.max(contentRect[hw], selectorRect[hw]); mainRect[yx] = Math.min(0, selectorRect[yx] + selectorPos[1 - orientIdx]); return mainRect; } else { contentGroup.attr('position', contentPos); return this.group.getBoundingRect(); } }, /** * @protected */ remove: function () { this.getContentGroup().removeAll(); this._isFirstRender = true; } }); function setSymbolStyle(symbol, symbolType, legendModelItemStyle, borderColor, inactiveBorderColor, isSelected) { var itemStyle; if (symbolType !== 'line' && symbolType.indexOf('empty') < 0) { itemStyle = legendModelItemStyle.getItemStyle(); symbol.style.stroke = borderColor; if (!isSelected) { itemStyle.stroke = inactiveBorderColor; } } else { itemStyle = legendModelItemStyle.getItemStyle(['borderWidth', 'borderColor']); } return symbol.setStyle(itemStyle); } function dispatchSelectAction(seriesName, dataName, api, excludeSeriesId) { // downplay before unselect dispatchDownplayAction(seriesName, dataName, api, excludeSeriesId); api.dispatchAction({ type: 'legendToggleSelect', name: seriesName != null ? seriesName : dataName }); // highlight after select dispatchHighlightAction(seriesName, dataName, api, excludeSeriesId); } function dispatchHighlightAction(seriesName, dataName, api, excludeSeriesId) { // If element hover will move to a hoverLayer. var el = api.getZr().storage.getDisplayList()[0]; if (!(el && el.useHoverLayer)) { api.dispatchAction({ type: 'highlight', seriesName: seriesName, name: dataName, excludeSeriesId: excludeSeriesId }); } } function dispatchDownplayAction(seriesName, dataName, api, excludeSeriesId) { // If element hover will move to a hoverLayer. var el = api.getZr().storage.getDisplayList()[0]; if (!(el && el.useHoverLayer)) { api.dispatchAction({ type: 'downplay', seriesName: seriesName, name: dataName, excludeSeriesId: excludeSeriesId }); } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var legendFilter = function (ecModel) { var legendModels = ecModel.findComponents({ mainType: 'legend' }); if (legendModels && legendModels.length) { ecModel.filterSeries(function (series) { // If in any legend component the status is not selected. // Because in legend series is assumed selected when it is not in the legend data. for (var i = 0; i < legendModels.length; i++) { if (!legendModels[i].isSelected(series.name)) { return false; } } return true; }); } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Do not contain scrollable legend, for sake of file size. // Series Filter registerProcessor(PRIORITY.PROCESSOR.SERIES_FILTER, legendFilter); ComponentModel.registerSubTypeDefaulter('legend', function () { // Default 'plain' when no type specified. return 'plain'; }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var ScrollableLegendModel = LegendModel.extend({ type: 'legend.scroll', /** * @param {number} scrollDataIndex */ setScrollDataIndex: function (scrollDataIndex) { this.option.scrollDataIndex = scrollDataIndex; }, defaultOption: { scrollDataIndex: 0, pageButtonItemGap: 5, pageButtonGap: null, pageButtonPosition: 'end', // 'start' or 'end' pageFormatter: '{current}/{total}', // If null/undefined, do not show page. pageIcons: { horizontal: ['M0,0L12,-10L12,10z', 'M0,0L-12,-10L-12,10z'], vertical: ['M0,0L20,0L10,-20z', 'M0,0L20,0L10,20z'] }, pageIconColor: '#2f4554', pageIconInactiveColor: '#aaa', pageIconSize: 15, // Can be [10, 3], which represents [width, height] pageTextStyle: { color: '#333' }, animationDurationUpdate: 800 }, /** * @override */ init: function (option, parentModel, ecModel, extraOpt) { var inputPositionParams = getLayoutParams(option); ScrollableLegendModel.superCall(this, 'init', option, parentModel, ecModel, extraOpt); mergeAndNormalizeLayoutParams(this, option, inputPositionParams); }, /** * @override */ mergeOption: function (option, extraOpt) { ScrollableLegendModel.superCall(this, 'mergeOption', option, extraOpt); mergeAndNormalizeLayoutParams(this, this.option, option); } }); // Do not `ignoreSize` to enable setting {left: 10, right: 10}. function mergeAndNormalizeLayoutParams(legendModel, target, raw) { var orient = legendModel.getOrient(); var ignoreSize = [1, 1]; ignoreSize[orient.index] = 0; mergeLayoutParam(target, raw, { type: 'box', ignoreSize: ignoreSize }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Separate legend and scrollable legend to reduce package size. */ var Group$3 = Group; var WH = ['width', 'height']; var XY = ['x', 'y']; var ScrollableLegendView = LegendView.extend({ type: 'legend.scroll', newlineDisabled: true, init: function () { ScrollableLegendView.superCall(this, 'init'); /** * @private * @type {number} For `scroll`. */ this._currentIndex = 0; /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._containerGroup = new Group$3()); this._containerGroup.add(this.getContentGroup()); /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._controllerGroup = new Group$3()); /** * * @private */ this._showController; }, /** * @override */ resetInner: function () { ScrollableLegendView.superCall(this, 'resetInner'); this._controllerGroup.removeAll(); this._containerGroup.removeClipPath(); this._containerGroup.__rectSize = null; }, /** * @override */ renderInner: function (itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition) { var me = this; // Render content items. ScrollableLegendView.superCall(this, 'renderInner', itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition); var controllerGroup = this._controllerGroup; // FIXME: support be 'auto' adapt to size number text length, // e.g., '3/12345' should not overlap with the control arrow button. var pageIconSize = legendModel.get('pageIconSize', true); if (!isArray(pageIconSize)) { pageIconSize = [pageIconSize, pageIconSize]; } createPageButton('pagePrev', 0); var pageTextStyleModel = legendModel.getModel('pageTextStyle'); controllerGroup.add(new Text({ name: 'pageText', style: { textFill: pageTextStyleModel.getTextColor(), font: pageTextStyleModel.getFont(), textVerticalAlign: 'middle', textAlign: 'center' }, silent: true })); createPageButton('pageNext', 1); function createPageButton(name, iconIdx) { var pageDataIndexName = name + 'DataIndex'; var icon = createIcon( legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx], { // Buttons will be created in each render, so we do not need // to worry about avoiding using legendModel kept in scope. onclick: bind( me._pageGo, me, pageDataIndexName, legendModel, api ) }, { x: -pageIconSize[0] / 2, y: -pageIconSize[1] / 2, width: pageIconSize[0], height: pageIconSize[1] } ); icon.name = name; controllerGroup.add(icon); } }, /** * @override */ layoutInner: function (legendModel, itemAlign, maxSize, isFirstRender, selector, selectorPosition) { var selectorGroup = this.getSelectorGroup(); var orientIdx = legendModel.getOrient().index; var wh = WH[orientIdx]; var xy = XY[orientIdx]; var hw = WH[1 - orientIdx]; var yx = XY[1 - orientIdx]; selector && box( // Buttons in selectorGroup always layout horizontally 'horizontal', selectorGroup, legendModel.get('selectorItemGap', true) ); var selectorButtonGap = legendModel.get('selectorButtonGap', true); var selectorRect = selectorGroup.getBoundingRect(); var selectorPos = [-selectorRect.x, -selectorRect.y]; var processMaxSize = clone(maxSize); selector && (processMaxSize[wh] = maxSize[wh] - selectorRect[wh] - selectorButtonGap); var mainRect = this._layoutContentAndController(legendModel, isFirstRender, processMaxSize, orientIdx, wh, hw, yx ); if (selector) { if (selectorPosition === 'end') { selectorPos[orientIdx] += mainRect[wh] + selectorButtonGap; } else { var offset = selectorRect[wh] + selectorButtonGap; selectorPos[orientIdx] -= offset; mainRect[xy] -= offset; } mainRect[wh] += selectorRect[wh] + selectorButtonGap; selectorPos[1 - orientIdx] += mainRect[yx] + mainRect[hw] / 2 - selectorRect[hw] / 2; mainRect[hw] = Math.max(mainRect[hw], selectorRect[hw]); mainRect[yx] = Math.min(mainRect[yx], selectorRect[yx] + selectorPos[1 - orientIdx]); selectorGroup.attr('position', selectorPos); } return mainRect; }, _layoutContentAndController: function (legendModel, isFirstRender, maxSize, orientIdx, wh, hw, yx) { var contentGroup = this.getContentGroup(); var containerGroup = this._containerGroup; var controllerGroup = this._controllerGroup; // Place items in contentGroup. box( legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), !orientIdx ? null : maxSize.width, orientIdx ? null : maxSize.height ); box( // Buttons in controller are layout always horizontally. 'horizontal', controllerGroup, legendModel.get('pageButtonItemGap', true) ); var contentRect = contentGroup.getBoundingRect(); var controllerRect = controllerGroup.getBoundingRect(); var showController = this._showController = contentRect[wh] > maxSize[wh]; var contentPos = [-contentRect.x, -contentRect.y]; // Remain contentPos when scroll animation perfroming. // If first rendering, `contentGroup.position` is [0, 0], which // does not make sense and may cause unexepcted animation if adopted. if (!isFirstRender) { contentPos[orientIdx] = contentGroup.position[orientIdx]; } // Layout container group based on 0. var containerPos = [0, 0]; var controllerPos = [-controllerRect.x, -controllerRect.y]; var pageButtonGap = retrieve2( legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true) ); // Place containerGroup and controllerGroup and contentGroup. if (showController) { var pageButtonPosition = legendModel.get('pageButtonPosition', true); // controller is on the right / bottom. if (pageButtonPosition === 'end') { controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh]; } // controller is on the left / top. else { containerPos[orientIdx] += controllerRect[wh] + pageButtonGap; } } // Always align controller to content as 'middle'. controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2; contentGroup.attr('position', contentPos); containerGroup.attr('position', containerPos); controllerGroup.attr('position', controllerPos); // Calculate `mainRect` and set `clipPath`. // mainRect should not be calculated by `this.group.getBoundingRect()` // for sake of the overflow. var mainRect = {x: 0, y: 0}; // Consider content may be overflow (should be clipped). mainRect[wh] = showController ? maxSize[wh] : contentRect[wh]; mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]); // `containerRect[yx] + containerPos[1 - orientIdx]` is 0. mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]); containerGroup.__rectSize = maxSize[wh]; if (showController) { var clipShape = {x: 0, y: 0}; clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0); clipShape[hw] = mainRect[hw]; containerGroup.setClipPath(new Rect({shape: clipShape})); // Consider content may be larger than container, container rect // can not be obtained from `containerGroup.getBoundingRect()`. containerGroup.__rectSize = clipShape[wh]; } else { // Do not remove or ignore controller. Keep them set as placeholders. controllerGroup.eachChild(function (child) { child.attr({invisible: true, silent: true}); }); } // Content translate animation. var pageInfo = this._getPageInfo(legendModel); pageInfo.pageIndex != null && updateProps( contentGroup, {position: pageInfo.contentPosition}, // When switch from "show controller" to "not show controller", view should be // updated immediately without animation, otherwise causes weird effect. showController ? legendModel : false ); this._updatePageInfoView(legendModel, pageInfo); return mainRect; }, _pageGo: function (to, legendModel, api) { var scrollDataIndex = this._getPageInfo(legendModel)[to]; scrollDataIndex != null && api.dispatchAction({ type: 'legendScroll', scrollDataIndex: scrollDataIndex, legendId: legendModel.id }); }, _updatePageInfoView: function (legendModel, pageInfo) { var controllerGroup = this._controllerGroup; each$1(['pagePrev', 'pageNext'], function (name) { var canJump = pageInfo[name + 'DataIndex'] != null; var icon = controllerGroup.childOfName(name); if (icon) { icon.setStyle( 'fill', canJump ? legendModel.get('pageIconColor', true) : legendModel.get('pageIconInactiveColor', true) ); icon.cursor = canJump ? 'pointer' : 'default'; } }); var pageText = controllerGroup.childOfName('pageText'); var pageFormatter = legendModel.get('pageFormatter'); var pageIndex = pageInfo.pageIndex; var current = pageIndex != null ? pageIndex + 1 : 0; var total = pageInfo.pageCount; pageText && pageFormatter && pageText.setStyle( 'text', isString(pageFormatter) ? pageFormatter.replace('{current}', current).replace('{total}', total) : pageFormatter({current: current, total: total}) ); }, /** * @param {module:echarts/model/Model} legendModel * @return {Object} { * contentPosition: Array., null when data item not found. * pageIndex: number, null when data item not found. * pageCount: number, always be a number, can be 0. * pagePrevDataIndex: number, null when no previous page. * pageNextDataIndex: number, null when no next page. * } */ _getPageInfo: function (legendModel) { var scrollDataIndex = legendModel.get('scrollDataIndex', true); var contentGroup = this.getContentGroup(); var containerRectSize = this._containerGroup.__rectSize; var orientIdx = legendModel.getOrient().index; var wh = WH[orientIdx]; var xy = XY[orientIdx]; var targetItemIndex = this._findTargetItemIndex(scrollDataIndex); var children = contentGroup.children(); var targetItem = children[targetItemIndex]; var itemCount = children.length; var pCount = !itemCount ? 0 : 1; var result = { contentPosition: contentGroup.position.slice(), pageCount: pCount, pageIndex: pCount - 1, pagePrevDataIndex: null, pageNextDataIndex: null }; if (!targetItem) { return result; } var targetItemInfo = getItemInfo(targetItem); result.contentPosition[orientIdx] = -targetItemInfo.s; // Strategy: // (1) Always align based on the left/top most item. // (2) It is user-friendly that the last item shown in the // current window is shown at the begining of next window. // Otherwise if half of the last item is cut by the window, // it will have no chance to display entirely. // (3) Consider that item size probably be different, we // have calculate pageIndex by size rather than item index, // and we can not get page index directly by division. // (4) The window is to narrow to contain more than // one item, we should make sure that the page can be fliped. for (var i = targetItemIndex + 1, winStartItemInfo = targetItemInfo, winEndItemInfo = targetItemInfo, currItemInfo = null; i <= itemCount; ++i ) { currItemInfo = getItemInfo(children[i]); if ( // Half of the last item is out of the window. (!currItemInfo && winEndItemInfo.e > winStartItemInfo.s + containerRectSize) // If the current item does not intersect with the window, the new page // can be started at the current item or the last item. || (currItemInfo && !intersect(currItemInfo, winStartItemInfo.s)) ) { if (winEndItemInfo.i > winStartItemInfo.i) { winStartItemInfo = winEndItemInfo; } else { // e.g., when page size is smaller than item size. winStartItemInfo = currItemInfo; } if (winStartItemInfo) { if (result.pageNextDataIndex == null) { result.pageNextDataIndex = winStartItemInfo.i; } ++result.pageCount; } } winEndItemInfo = currItemInfo; } for (var i = targetItemIndex - 1, winStartItemInfo = targetItemInfo, winEndItemInfo = targetItemInfo, currItemInfo = null; i >= -1; --i ) { currItemInfo = getItemInfo(children[i]); if ( // If the the end item does not intersect with the window started // from the current item, a page can be settled. (!currItemInfo || !intersect(winEndItemInfo, currItemInfo.s)) // e.g., when page size is smaller than item size. && winStartItemInfo.i < winEndItemInfo.i ) { winEndItemInfo = winStartItemInfo; if (result.pagePrevDataIndex == null) { result.pagePrevDataIndex = winStartItemInfo.i; } ++result.pageCount; ++result.pageIndex; } winStartItemInfo = currItemInfo; } return result; function getItemInfo(el) { if (el) { var itemRect = el.getBoundingRect(); var start = itemRect[xy] + el.position[orientIdx]; return { s: start, e: start + itemRect[wh], i: el.__legendDataIndex }; } } function intersect(itemInfo, winStart) { return itemInfo.e >= winStart && itemInfo.s <= winStart + containerRectSize; } }, _findTargetItemIndex: function (targetDataIndex) { if (!this._showController) { return 0; } var index; var contentGroup = this.getContentGroup(); var defaultIndex; contentGroup.eachChild(function (child, idx) { var legendDataIdx = child.__legendDataIndex; // FIXME // If the given targetDataIndex (from model) is illegal, // we use defaultIndex. But the index on the legend model and // action payload is still illegal. That case will not be // changed until some scenario requires. if (defaultIndex == null && legendDataIdx != null) { defaultIndex = idx; } if (legendDataIdx === targetDataIndex) { index = idx; } }); return index != null ? index : defaultIndex; } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @event legendScroll * @type {Object} * @property {string} type 'legendScroll' * @property {string} scrollDataIndex */ registerAction( 'legendScroll', 'legendscroll', function (payload, ecModel) { var scrollDataIndex = payload.scrollDataIndex; scrollDataIndex != null && ecModel.eachComponent( {mainType: 'legend', subType: 'scroll', query: payload}, function (legendModel) { legendModel.setScrollDataIndex(scrollDataIndex); } ); } ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Legend component entry file8 */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Model extendComponentModel({ type: 'title', layoutMode: {type: 'box', ignoreSize: true}, defaultOption: { // 一级层叠 zlevel: 0, // 二级层叠 z: 6, show: true, text: '', // 超链接跳转 // link: null, // 仅支持self | blank target: 'blank', subtext: '', // 超链接跳转 // sublink: null, // 仅支持self | blank subtarget: 'blank', // 'center' ¦ 'left' ¦ 'right' // ¦ {number}(x坐标,单位px) left: 0, // 'top' ¦ 'bottom' ¦ 'center' // ¦ {number}(y坐标,单位px) top: 0, // 水平对齐 // 'auto' | 'left' | 'right' | 'center' // 默认根据 left 的位置判断是左对齐还是右对齐 // textAlign: null // // 垂直对齐 // 'auto' | 'top' | 'bottom' | 'middle' // 默认根据 top 位置判断是上对齐还是下对齐 // textVerticalAlign: null // textBaseline: null // The same as textVerticalAlign. backgroundColor: 'rgba(0,0,0,0)', // 标题边框颜色 borderColor: '#ccc', // 标题边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 标题内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // 主副标题纵向间隔,单位px,默认为10, itemGap: 10, textStyle: { fontSize: 18, fontWeight: 'bolder', color: '#333' }, subtextStyle: { color: '#aaa' } } }); // View extendComponentView({ type: 'title', render: function (titleModel, ecModel, api) { this.group.removeAll(); if (!titleModel.get('show')) { return; } var group = this.group; var textStyleModel = titleModel.getModel('textStyle'); var subtextStyleModel = titleModel.getModel('subtextStyle'); var textAlign = titleModel.get('textAlign'); var textVerticalAlign = retrieve2( titleModel.get('textBaseline'), titleModel.get('textVerticalAlign') ); var textEl = new Text({ style: setTextStyle({}, textStyleModel, { text: titleModel.get('text'), textFill: textStyleModel.getTextColor() }, {disableBox: true}), z2: 10 }); var textRect = textEl.getBoundingRect(); var subText = titleModel.get('subtext'); var subTextEl = new Text({ style: setTextStyle({}, subtextStyleModel, { text: subText, textFill: subtextStyleModel.getTextColor(), y: textRect.height + titleModel.get('itemGap'), textVerticalAlign: 'top' }, {disableBox: true}), z2: 10 }); var link = titleModel.get('link'); var sublink = titleModel.get('sublink'); var triggerEvent = titleModel.get('triggerEvent', true); textEl.silent = !link && !triggerEvent; subTextEl.silent = !sublink && !triggerEvent; if (link) { textEl.on('click', function () { windowOpen(link, '_' + titleModel.get('target')); }); } if (sublink) { subTextEl.on('click', function () { windowOpen(sublink, '_' + titleModel.get('subtarget')); }); } textEl.eventData = subTextEl.eventData = triggerEvent ? { componentType: 'title', componentIndex: titleModel.componentIndex } : null; group.add(textEl); subText && group.add(subTextEl); // If no subText, but add subTextEl, there will be an empty line. var groupRect = group.getBoundingRect(); var layoutOption = titleModel.getBoxLayoutParams(); layoutOption.width = groupRect.width; layoutOption.height = groupRect.height; var layoutRect = getLayoutRect( layoutOption, { width: api.getWidth(), height: api.getHeight() }, titleModel.get('padding') ); // Adjust text align based on position if (!textAlign) { // Align left if title is on the left. center and right is same textAlign = titleModel.get('left') || titleModel.get('right'); if (textAlign === 'middle') { textAlign = 'center'; } // Adjust layout by text align if (textAlign === 'right') { layoutRect.x += layoutRect.width; } else if (textAlign === 'center') { layoutRect.x += layoutRect.width / 2; } } if (!textVerticalAlign) { textVerticalAlign = titleModel.get('top') || titleModel.get('bottom'); if (textVerticalAlign === 'center') { textVerticalAlign = 'middle'; } if (textVerticalAlign === 'bottom') { layoutRect.y += layoutRect.height; } else if (textVerticalAlign === 'middle') { layoutRect.y += layoutRect.height / 2; } textVerticalAlign = textVerticalAlign || 'top'; } group.attr('position', [layoutRect.x, layoutRect.y]); var alignStyle = { textAlign: textAlign, textVerticalAlign: textVerticalAlign }; textEl.setStyle(alignStyle); subTextEl.setStyle(alignStyle); // Render background // Get groupRect again because textAlign has been changed groupRect = group.getBoundingRect(); var padding = layoutRect.margin; var style = titleModel.getItemStyle(['color', 'opacity']); style.fill = titleModel.get('backgroundColor'); var rect = new Rect({ shape: { x: groupRect.x - padding[3], y: groupRect.y - padding[0], width: groupRect.width + padding[1] + padding[3], height: groupRect.height + padding[0] + padding[2], r: titleModel.get('borderRadius') }, style: style, subPixelOptimize: true, silent: true }); group.add(rect); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var addCommas$1 = addCommas; var encodeHTML$1 = encodeHTML; function fillLabel(opt) { defaultEmphasis(opt, 'label', ['show']); } var MarkerModel = extendComponentModel({ type: 'marker', dependencies: ['series', 'grid', 'polar', 'geo'], /** * @overrite */ init: function (option, parentModel, ecModel) { if (__DEV__) { if (this.type === 'marker') { throw new Error('Marker component is abstract component. Use markLine, markPoint, markArea instead.'); } } this.mergeDefaultAndTheme(option, ecModel); this._mergeOption(option, ecModel, false, true); }, /** * @return {boolean} */ isAnimationEnabled: function () { if (env$1.node) { return false; } var hostSeries = this.__hostSeries; return this.getShallow('animation') && hostSeries && hostSeries.isAnimationEnabled(); }, /** * @overrite */ mergeOption: function (newOpt, ecModel) { this._mergeOption(newOpt, ecModel, false, false); }, _mergeOption: function (newOpt, ecModel, createdBySelf, isInit) { var MarkerModel = this.constructor; var modelPropName = this.mainType + 'Model'; if (!createdBySelf) { ecModel.eachSeries(function (seriesModel) { var markerOpt = seriesModel.get(this.mainType, true); var markerModel = seriesModel[modelPropName]; if (!markerOpt || !markerOpt.data) { seriesModel[modelPropName] = null; return; } if (!markerModel) { if (isInit) { // Default label emphasis `position` and `show` fillLabel(markerOpt); } each$1(markerOpt.data, function (item) { // FIXME Overwrite fillLabel method ? if (item instanceof Array) { fillLabel(item[0]); fillLabel(item[1]); } else { fillLabel(item); } }); markerModel = new MarkerModel( markerOpt, this, ecModel ); extend(markerModel, { mainType: this.mainType, // Use the same series index and name seriesIndex: seriesModel.seriesIndex, name: seriesModel.name, createdBySelf: true }); markerModel.__hostSeries = seriesModel; } else { markerModel._mergeOption(markerOpt, ecModel, true); } seriesModel[modelPropName] = markerModel; }, this); } }, formatTooltip: function (dataIndex, multipleSeries, dataType, renderMode) { var data = this.getData(); var value = this.getRawValue(dataIndex); var formattedValue = isArray(value) ? map(value, addCommas$1).join(', ') : addCommas$1(value); var name = data.getName(dataIndex); var html = encodeHTML$1(this.name); var newLine = renderMode === 'html' ? '
' : '\n'; if (value != null || name) { html += newLine; } if (name) { html += encodeHTML$1(name); if (value != null) { html += ' : '; } } if (value != null) { html += encodeHTML$1(formattedValue); } return html; }, getData: function () { return this._data; }, setData: function (data) { this._data = data; } }); mixin(MarkerModel, dataFormatMixin); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ MarkerModel.extend({ type: 'markPoint', defaultOption: { zlevel: 0, z: 5, symbol: 'pin', symbolSize: 50, //symbolRotate: 0, //symbolOffset: [0, 0] tooltip: { trigger: 'item' }, label: { show: true, position: 'inside' }, itemStyle: { borderWidth: 2 }, emphasis: { label: { show: true } } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var indexOf$1 = indexOf; function hasXOrY(item) { return !(isNaN(parseFloat(item.x)) && isNaN(parseFloat(item.y))); } function hasXAndY(item) { return !isNaN(parseFloat(item.x)) && !isNaN(parseFloat(item.y)); } // Make it simple, do not visit all stacked value to count precision. // function getPrecision(data, valueAxisDim, dataIndex) { // var precision = -1; // var stackedDim = data.mapDimension(valueAxisDim); // do { // precision = Math.max( // numberUtil.getPrecision(data.get(stackedDim, dataIndex)), // precision // ); // var stackedOnSeries = data.getCalculationInfo('stackedOnSeries'); // if (stackedOnSeries) { // var byValue = data.get(data.getCalculationInfo('stackedByDimension'), dataIndex); // data = stackedOnSeries.getData(); // dataIndex = data.indexOf(data.getCalculationInfo('stackedByDimension'), byValue); // stackedDim = data.getCalculationInfo('stackedDimension'); // } // else { // data = null; // } // } while (data); // return precision; // } function markerTypeCalculatorWithExtent( mlType, data, otherDataDim, targetDataDim, otherCoordIndex, targetCoordIndex ) { var coordArr = []; var stacked = isDimensionStacked(data, targetDataDim /*, otherDataDim*/); var calcDataDim = stacked ? data.getCalculationInfo('stackResultDimension') : targetDataDim; var value = numCalculate(data, calcDataDim, mlType); var dataIndex = data.indicesOfNearest(calcDataDim, value)[0]; coordArr[otherCoordIndex] = data.get(otherDataDim, dataIndex); coordArr[targetCoordIndex] = data.get(calcDataDim, dataIndex); var coordArrValue = data.get(targetDataDim, dataIndex); // Make it simple, do not visit all stacked value to count precision. var precision = getPrecision(data.get(targetDataDim, dataIndex)); precision = Math.min(precision, 20); if (precision >= 0) { coordArr[targetCoordIndex] = +coordArr[targetCoordIndex].toFixed(precision); } return [coordArr, coordArrValue]; } var curry$4 = curry; // TODO Specified percent var markerTypeCalculator = { /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ min: curry$4(markerTypeCalculatorWithExtent, 'min'), /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ max: curry$4(markerTypeCalculatorWithExtent, 'max'), /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ average: curry$4(markerTypeCalculatorWithExtent, 'average') }; /** * Transform markPoint data item to format used in List by do the following * 1. Calculate statistic like `max`, `min`, `average` * 2. Convert `item.xAxis`, `item.yAxis` to `item.coord` array * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/coord/*} [coordSys] * @param {Object} item * @return {Object} */ function dataTransform(seriesModel, item) { var data = seriesModel.getData(); var coordSys = seriesModel.coordinateSystem; // 1. If not specify the position with pixel directly // 2. If `coord` is not a data array. Which uses `xAxis`, // `yAxis` to specify the coord on each dimension // parseFloat first because item.x and item.y can be percent string like '20%' if (item && !hasXAndY(item) && !isArray(item.coord) && coordSys) { var dims = coordSys.dimensions; var axisInfo = getAxisInfo$1(item, data, coordSys, seriesModel); // Clone the option // Transform the properties xAxis, yAxis, radiusAxis, angleAxis, geoCoord to value item = clone(item); if (item.type && markerTypeCalculator[item.type] && axisInfo.baseAxis && axisInfo.valueAxis ) { var otherCoordIndex = indexOf$1(dims, axisInfo.baseAxis.dim); var targetCoordIndex = indexOf$1(dims, axisInfo.valueAxis.dim); var coordInfo = markerTypeCalculator[item.type]( data, axisInfo.baseDataDim, axisInfo.valueDataDim, otherCoordIndex, targetCoordIndex ); item.coord = coordInfo[0]; // Force to use the value of calculated value. // let item use the value without stack. item.value = coordInfo[1]; } else { // FIXME Only has one of xAxis and yAxis. var coord = [ item.xAxis != null ? item.xAxis : item.radiusAxis, item.yAxis != null ? item.yAxis : item.angleAxis ]; // Each coord support max, min, average for (var i = 0; i < 2; i++) { if (markerTypeCalculator[coord[i]]) { coord[i] = numCalculate(data, data.mapDimension(dims[i]), coord[i]); } } item.coord = coord; } } return item; } function getAxisInfo$1(item, data, coordSys, seriesModel) { var ret = {}; if (item.valueIndex != null || item.valueDim != null) { ret.valueDataDim = item.valueIndex != null ? data.getDimension(item.valueIndex) : item.valueDim; ret.valueAxis = coordSys.getAxis(dataDimToCoordDim(seriesModel, ret.valueDataDim)); ret.baseAxis = coordSys.getOtherAxis(ret.valueAxis); ret.baseDataDim = data.mapDimension(ret.baseAxis.dim); } else { ret.baseAxis = seriesModel.getBaseAxis(); ret.valueAxis = coordSys.getOtherAxis(ret.baseAxis); ret.baseDataDim = data.mapDimension(ret.baseAxis.dim); ret.valueDataDim = data.mapDimension(ret.valueAxis.dim); } return ret; } function dataDimToCoordDim(seriesModel, dataDim) { var data = seriesModel.getData(); var dimensions = data.dimensions; dataDim = data.getDimension(dataDim); for (var i = 0; i < dimensions.length; i++) { var dimItem = data.getDimensionInfo(dimensions[i]); if (dimItem.name === dataDim) { return dimItem.coordDim; } } } /** * Filter data which is out of coordinateSystem range * [dataFilter description] * @param {module:echarts/coord/*} [coordSys] * @param {Object} item * @return {boolean} */ function dataFilter$1(coordSys, item) { // Alwalys return true if there is no coordSys return (coordSys && coordSys.containData && item.coord && !hasXOrY(item)) ? coordSys.containData(item.coord) : true; } function dimValueGetter(item, dimName, dataIndex, dimIndex) { // x, y, radius, angle if (dimIndex < 2) { return item.coord && item.coord[dimIndex]; } return item.value; } function numCalculate(data, valueDataDim, type) { if (type === 'average') { var sum = 0; var count = 0; data.each(valueDataDim, function (val, idx) { if (!isNaN(val)) { sum += val; count++; } }); return sum / count; } else if (type === 'median') { return data.getMedian(valueDataDim); } else { // max & min return data.getDataExtent(valueDataDim, true)[type === 'max' ? 1 : 0]; } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var MarkerView = extendComponentView({ type: 'marker', init: function () { /** * Markline grouped by series * @private * @type {module:zrender/core/util.HashMap} */ this.markerGroupMap = createHashMap(); }, render: function (markerModel, ecModel, api) { var markerGroupMap = this.markerGroupMap; markerGroupMap.each(function (item) { item.__keep = false; }); var markerModelKey = this.type + 'Model'; ecModel.eachSeries(function (seriesModel) { var markerModel = seriesModel[markerModelKey]; markerModel && this.renderSeries(seriesModel, markerModel, ecModel, api); }, this); markerGroupMap.each(function (item) { !item.__keep && this.group.remove(item.group); }, this); }, renderSeries: function () {} }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function updateMarkerLayout(mpData, seriesModel, api) { var coordSys = seriesModel.coordinateSystem; mpData.each(function (idx) { var itemModel = mpData.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get('x'), api.getWidth()); var yPx = parsePercent$1(itemModel.get('y'), api.getHeight()); if (!isNaN(xPx) && !isNaN(yPx)) { point = [xPx, yPx]; } // Chart like bar may have there own marker positioning logic else if (seriesModel.getMarkerPosition) { // Use the getMarkerPoisition point = seriesModel.getMarkerPosition( mpData.getValues(mpData.dimensions, idx) ); } else if (coordSys) { var x = mpData.get(coordSys.dimensions[0], idx); var y = mpData.get(coordSys.dimensions[1], idx); point = coordSys.dataToPoint([x, y]); } // Use x, y if has any if (!isNaN(xPx)) { point[0] = xPx; } if (!isNaN(yPx)) { point[1] = yPx; } mpData.setItemLayout(idx, point); }); } MarkerView.extend({ type: 'markPoint', // updateLayout: function (markPointModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var mpModel = seriesModel.markPointModel; // if (mpModel) { // updateMarkerLayout(mpModel.getData(), seriesModel, api); // this.markerGroupMap.get(seriesModel.id).updateLayout(mpModel); // } // }, this); // }, updateTransform: function (markPointModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var mpModel = seriesModel.markPointModel; if (mpModel) { updateMarkerLayout(mpModel.getData(), seriesModel, api); this.markerGroupMap.get(seriesModel.id).updateLayout(mpModel); } }, this); }, renderSeries: function (seriesModel, mpModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesId = seriesModel.id; var seriesData = seriesModel.getData(); var symbolDrawMap = this.markerGroupMap; var symbolDraw = symbolDrawMap.get(seriesId) || symbolDrawMap.set(seriesId, new SymbolDraw()); var mpData = createList$1(coordSys, seriesModel, mpModel); // FIXME mpModel.setData(mpData); updateMarkerLayout(mpModel.getData(), seriesModel, api); mpData.each(function (idx) { var itemModel = mpData.getItemModel(idx); var symbol = itemModel.getShallow('symbol'); var symbolSize = itemModel.getShallow('symbolSize'); var symbolRotate = itemModel.getShallow('symbolRotate'); var isFnSymbol = isFunction$1(symbol); var isFnSymbolSize = isFunction$1(symbolSize); var isFnSymbolRotate = isFunction$1(symbolRotate); if (isFnSymbol || isFnSymbolSize || isFnSymbolRotate) { var rawIdx = mpModel.getRawValue(idx); var dataParams = mpModel.getDataParams(idx); if (isFnSymbol) { symbol = symbol(rawIdx, dataParams); } if (isFnSymbolSize) { // FIXME 这里不兼容 ECharts 2.x,2.x 貌似参数是整个数据? symbolSize = symbolSize(rawIdx, dataParams); } if (isFnSymbolRotate) { symbolRotate = symbolRotate(rawIdx, dataParams); } } mpData.setItemVisual(idx, { symbol: symbol, symbolSize: symbolSize, symbolRotate: symbolRotate, color: itemModel.get('itemStyle.color') || seriesData.getVisual('color') }); }); // TODO Text are wrong symbolDraw.updateData(mpData); this.group.add(symbolDraw.group); // Set host model for tooltip // FIXME mpData.eachItemGraphicEl(function (el) { el.traverse(function (child) { child.dataModel = mpModel; }); }); symbolDraw.__keep = true; symbolDraw.group.silent = mpModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} [coordSys] * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$1(coordSys, seriesModel, mpModel) { var coordDimsInfos; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var info = seriesModel.getData().getDimensionInfo( seriesModel.getData().mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys return defaults({name: coordDim}, info); }); } else { coordDimsInfos = [{ name: 'value', type: 'float' }]; } var mpData = new List(coordDimsInfos, mpModel); var dataOpt = map(mpModel.get('data'), curry( dataTransform, seriesModel )); if (coordSys) { dataOpt = filter( dataOpt, curry(dataFilter$1, coordSys) ); } mpData.initData(dataOpt, null, coordSys ? dimValueGetter : function (item) { return item.value; } ); return mpData; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // HINT Markpoint can't be used too much registerPreprocessor(function (opt) { // Make sure markPoint component is enabled opt.markPoint = opt.markPoint || {}; }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ MarkerModel.extend({ type: 'markLine', defaultOption: { zlevel: 0, z: 5, symbol: ['circle', 'arrow'], symbolSize: [8, 16], //symbolRotate: 0, precision: 2, tooltip: { trigger: 'item' }, label: { show: true, position: 'end', distance: 5 }, lineStyle: { type: 'dashed' }, emphasis: { label: { show: true }, lineStyle: { width: 3 } }, animationEasing: 'linear' } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Line path for bezier and straight line draw */ var straightLineProto = Line.prototype; var bezierCurveProto = BezierCurve.prototype; function isLine(shape) { return isNaN(+shape.cpx1) || isNaN(+shape.cpy1); } var LinePath = extendShape({ type: 'ec-line', style: { stroke: '#000', fill: null }, shape: { x1: 0, y1: 0, x2: 0, y2: 0, percent: 1, cpx1: null, cpy1: null }, buildPath: function (ctx, shape) { this[isLine(shape) ? '_buildPathLine' : '_buildPathCurve'](ctx, shape); }, _buildPathLine: straightLineProto.buildPath, _buildPathCurve: bezierCurveProto.buildPath, pointAt: function (t) { return this[isLine(this.shape) ? '_pointAtLine' : '_pointAtCurve'](t); }, _pointAtLine: straightLineProto.pointAt, _pointAtCurve: bezierCurveProto.pointAt, tangentAt: function (t) { var shape = this.shape; var p = isLine(shape) ? [shape.x2 - shape.x1, shape.y2 - shape.y1] : this._tangentAtCurve(t); return normalize(p, p); }, _tangentAtCurve: bezierCurveProto.tangentAt }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/chart/helper/Line */ var SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol']; function makeSymbolTypeKey(symbolCategory) { return '_' + symbolCategory + 'Type'; } /** * @inner */ function createSymbol$1(name, lineData, idx) { var symbolType = lineData.getItemVisual(idx, name); if (!symbolType || symbolType === 'none') { return; } var color = lineData.getItemVisual(idx, 'color'); var symbolSize = lineData.getItemVisual(idx, name + 'Size'); var symbolRotate = lineData.getItemVisual(idx, name + 'Rotate'); if (!isArray(symbolSize)) { symbolSize = [symbolSize, symbolSize]; } var symbolPath = createSymbol( symbolType, -symbolSize[0] / 2, -symbolSize[1] / 2, symbolSize[0], symbolSize[1], color ); // rotate by default if symbolRotate is not specified or NaN symbolPath.__specifiedRotation = symbolRotate == null || isNaN(symbolRotate) ? void 0 : +symbolRotate * Math.PI / 180 || 0; symbolPath.name = name; return symbolPath; } function createLine(points) { var line = new LinePath({ name: 'line', subPixelOptimize: true }); setLinePoints(line.shape, points); return line; } function setLinePoints(targetShape, points) { targetShape.x1 = points[0][0]; targetShape.y1 = points[0][1]; targetShape.x2 = points[1][0]; targetShape.y2 = points[1][1]; targetShape.percent = 1; var cp1 = points[2]; if (cp1) { targetShape.cpx1 = cp1[0]; targetShape.cpy1 = cp1[1]; } else { targetShape.cpx1 = NaN; targetShape.cpy1 = NaN; } } function updateSymbolAndLabelBeforeLineUpdate() { var lineGroup = this; var symbolFrom = lineGroup.childOfName('fromSymbol'); var symbolTo = lineGroup.childOfName('toSymbol'); var label = lineGroup.childOfName('label'); // Quick reject if (!symbolFrom && !symbolTo && label.ignore) { return; } var invScale = 1; var parentNode = this.parent; while (parentNode) { if (parentNode.scale) { invScale /= parentNode.scale[0]; } parentNode = parentNode.parent; } var line = lineGroup.childOfName('line'); // If line not changed // FIXME Parent scale changed if (!this.__dirty && !line.__dirty) { return; } var percent = line.shape.percent; var fromPos = line.pointAt(0); var toPos = line.pointAt(percent); var d = sub([], toPos, fromPos); normalize(d, d); if (symbolFrom) { symbolFrom.attr('position', fromPos); // Fix #12388 // when symbol is set to be 'arrow' in markLine, // symbolRotate value will be ignored, and compulsively use tangent angle. // rotate by default if symbol rotation is not specified var specifiedRotation = symbolFrom.__specifiedRotation; if (specifiedRotation == null) { var tangent = line.tangentAt(0); symbolFrom.attr('rotation', Math.PI / 2 - Math.atan2( tangent[1], tangent[0] )); } else { symbolFrom.attr('rotation', specifiedRotation); } symbolFrom.attr('scale', [invScale * percent, invScale * percent]); } if (symbolTo) { symbolTo.attr('position', toPos); // Fix #12388 // when symbol is set to be 'arrow' in markLine, // symbolRotate value will be ignored, and compulsively use tangent angle. // rotate by default if symbol rotation is not specified var specifiedRotation = symbolTo.__specifiedRotation; if (specifiedRotation == null) { var tangent = line.tangentAt(1); symbolTo.attr('rotation', -Math.PI / 2 - Math.atan2( tangent[1], tangent[0] )); } else { symbolTo.attr('rotation', specifiedRotation); } symbolTo.attr('scale', [invScale * percent, invScale * percent]); } if (!label.ignore) { label.attr('position', toPos); var textPosition; var textAlign; var textVerticalAlign; var textOrigin; var distance$$1 = label.__labelDistance; var distanceX = distance$$1[0] * invScale; var distanceY = distance$$1[1] * invScale; var halfPercent = percent / 2; var tangent = line.tangentAt(halfPercent); var n = [tangent[1], -tangent[0]]; var cp = line.pointAt(halfPercent); if (n[1] > 0) { n[0] = -n[0]; n[1] = -n[1]; } var dir = tangent[0] < 0 ? -1 : 1; if (label.__position !== 'start' && label.__position !== 'end') { var rotation = -Math.atan2(tangent[1], tangent[0]); if (toPos[0] < fromPos[0]) { rotation = Math.PI + rotation; } label.attr('rotation', rotation); } var dy; switch (label.__position) { case 'insideStartTop': case 'insideMiddleTop': case 'insideEndTop': case 'middle': dy = -distanceY; textVerticalAlign = 'bottom'; break; case 'insideStartBottom': case 'insideMiddleBottom': case 'insideEndBottom': dy = distanceY; textVerticalAlign = 'top'; break; default: dy = 0; textVerticalAlign = 'middle'; } switch (label.__position) { case 'end': textPosition = [d[0] * distanceX + toPos[0], d[1] * distanceY + toPos[1]]; textAlign = d[0] > 0.8 ? 'left' : (d[0] < -0.8 ? 'right' : 'center'); textVerticalAlign = d[1] > 0.8 ? 'top' : (d[1] < -0.8 ? 'bottom' : 'middle'); break; case 'start': textPosition = [-d[0] * distanceX + fromPos[0], -d[1] * distanceY + fromPos[1]]; textAlign = d[0] > 0.8 ? 'right' : (d[0] < -0.8 ? 'left' : 'center'); textVerticalAlign = d[1] > 0.8 ? 'bottom' : (d[1] < -0.8 ? 'top' : 'middle'); break; case 'insideStartTop': case 'insideStart': case 'insideStartBottom': textPosition = [distanceX * dir + fromPos[0], fromPos[1] + dy]; textAlign = tangent[0] < 0 ? 'right' : 'left'; textOrigin = [-distanceX * dir, -dy]; break; case 'insideMiddleTop': case 'insideMiddle': case 'insideMiddleBottom': case 'middle': textPosition = [cp[0], cp[1] + dy]; textAlign = 'center'; textOrigin = [0, -dy]; break; case 'insideEndTop': case 'insideEnd': case 'insideEndBottom': textPosition = [-distanceX * dir + toPos[0], toPos[1] + dy]; textAlign = tangent[0] >= 0 ? 'right' : 'left'; textOrigin = [distanceX * dir, -dy]; break; } label.attr({ style: { // Use the user specified text align and baseline first textVerticalAlign: label.__verticalAlign || textVerticalAlign, textAlign: label.__textAlign || textAlign }, position: textPosition, scale: [invScale, invScale], origin: textOrigin }); } } /** * @constructor * @extends {module:zrender/graphic/Group} * @alias {module:echarts/chart/helper/Line} */ function Line$1(lineData, idx, seriesScope) { Group.call(this); this._createLine(lineData, idx, seriesScope); } var lineProto = Line$1.prototype; // Update symbol position and rotation lineProto.beforeUpdate = updateSymbolAndLabelBeforeLineUpdate; lineProto._createLine = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var linePoints = lineData.getItemLayout(idx); var line = createLine(linePoints); line.shape.percent = 0; initProps(line, { shape: { percent: 1 } }, seriesModel, idx); this.add(line); var label = new Text({ name: 'label', // FIXME // Temporary solution for `focusNodeAdjacency`. // line label do not use the opacity of lineStyle. lineLabelOriginalOpacity: 1 }); this.add(label); each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbol = createSymbol$1(symbolCategory, lineData, idx); // symbols must added after line to make sure // it will be updated after line#update. // Or symbol position and rotation update in line#beforeUpdate will be one frame slow this.add(symbol); this[makeSymbolTypeKey(symbolCategory)] = lineData.getItemVisual(idx, symbolCategory); }, this); this._updateCommonStl(lineData, idx, seriesScope); }; lineProto.updateData = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var line = this.childOfName('line'); var linePoints = lineData.getItemLayout(idx); var target = { shape: {} }; setLinePoints(target.shape, linePoints); updateProps(line, target, seriesModel, idx); each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbolType = lineData.getItemVisual(idx, symbolCategory); var key = makeSymbolTypeKey(symbolCategory); // Symbol changed if (this[key] !== symbolType) { this.remove(this.childOfName(symbolCategory)); var symbol = createSymbol$1(symbolCategory, lineData, idx); this.add(symbol); } this[key] = symbolType; }, this); this._updateCommonStl(lineData, idx, seriesScope); }; lineProto._updateCommonStl = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var line = this.childOfName('line'); var lineStyle = seriesScope && seriesScope.lineStyle; var hoverLineStyle = seriesScope && seriesScope.hoverLineStyle; var labelModel = seriesScope && seriesScope.labelModel; var hoverLabelModel = seriesScope && seriesScope.hoverLabelModel; // Optimization for large dataset if (!seriesScope || lineData.hasItemOption) { var itemModel = lineData.getItemModel(idx); lineStyle = itemModel.getModel('lineStyle').getLineStyle(); hoverLineStyle = itemModel.getModel('emphasis.lineStyle').getLineStyle(); labelModel = itemModel.getModel('label'); hoverLabelModel = itemModel.getModel('emphasis.label'); } var visualColor = lineData.getItemVisual(idx, 'color'); var visualOpacity = retrieve3( lineData.getItemVisual(idx, 'opacity'), lineStyle.opacity, 1 ); line.useStyle(defaults( { strokeNoScale: true, fill: 'none', stroke: visualColor, opacity: visualOpacity }, lineStyle )); line.hoverStyle = hoverLineStyle; // Update symbol each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbol = this.childOfName(symbolCategory); if (symbol) { symbol.setColor(visualColor); symbol.setStyle({ opacity: visualOpacity }); } }, this); var showLabel = labelModel.getShallow('show'); var hoverShowLabel = hoverLabelModel.getShallow('show'); var label = this.childOfName('label'); var defaultLabelColor; var baseText; // FIXME: the logic below probably should be merged to `graphic.setLabelStyle`. if (showLabel || hoverShowLabel) { defaultLabelColor = visualColor || '#000'; baseText = seriesModel.getFormattedLabel(idx, 'normal', lineData.dataType); if (baseText == null) { var rawVal = seriesModel.getRawValue(idx); baseText = rawVal == null ? lineData.getName(idx) : isFinite(rawVal) ? round$1(rawVal) : rawVal; } } var normalText = showLabel ? baseText : null; var emphasisText = hoverShowLabel ? retrieve2( seriesModel.getFormattedLabel(idx, 'emphasis', lineData.dataType), baseText ) : null; var labelStyle = label.style; // Always set `textStyle` even if `normalStyle.text` is null, because default // values have to be set on `normalStyle`. if (normalText != null || emphasisText != null) { setTextStyle(label.style, labelModel, { text: normalText }, { autoColor: defaultLabelColor }); label.__textAlign = labelStyle.textAlign; label.__verticalAlign = labelStyle.textVerticalAlign; // 'start', 'middle', 'end' label.__position = labelModel.get('position') || 'middle'; var distance$$1 = labelModel.get('distance'); if (!isArray(distance$$1)) { distance$$1 = [distance$$1, distance$$1]; } label.__labelDistance = distance$$1; } if (emphasisText != null) { // Only these properties supported in this emphasis style here. label.hoverStyle = { text: emphasisText, textFill: hoverLabelModel.getTextColor(true), // For merging hover style to normal style, do not use // `hoverLabelModel.getFont()` here. fontStyle: hoverLabelModel.getShallow('fontStyle'), fontWeight: hoverLabelModel.getShallow('fontWeight'), fontSize: hoverLabelModel.getShallow('fontSize'), fontFamily: hoverLabelModel.getShallow('fontFamily') }; } else { label.hoverStyle = { text: null }; } label.ignore = !showLabel && !hoverShowLabel; setHoverStyle(this); }; lineProto.highlight = function () { this.trigger('emphasis'); }; lineProto.downplay = function () { this.trigger('normal'); }; lineProto.updateLayout = function (lineData, idx) { this.setLinePoints(lineData.getItemLayout(idx)); }; lineProto.setLinePoints = function (points) { var linePath = this.childOfName('line'); setLinePoints(linePath.shape, points); linePath.dirty(); }; inherits(Line$1, Group); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @module echarts/chart/helper/LineDraw */ // import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; /** * @alias module:echarts/component/marker/LineDraw * @constructor */ function LineDraw(ctor) { this._ctor = ctor || Line$1; this.group = new Group(); } var lineDrawProto = LineDraw.prototype; lineDrawProto.isPersistent = function () { return true; }; /** * @param {module:echarts/data/List} lineData */ lineDrawProto.updateData = function (lineData) { var lineDraw = this; var group = lineDraw.group; var oldLineData = lineDraw._lineData; lineDraw._lineData = lineData; // There is no oldLineData only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!oldLineData) { group.removeAll(); } var seriesScope = makeSeriesScope$1(lineData); lineData.diff(oldLineData) .add(function (idx) { doAdd(lineDraw, lineData, idx, seriesScope); }) .update(function (newIdx, oldIdx) { doUpdate(lineDraw, oldLineData, lineData, oldIdx, newIdx, seriesScope); }) .remove(function (idx) { group.remove(oldLineData.getItemGraphicEl(idx)); }) .execute(); }; function doAdd(lineDraw, lineData, idx, seriesScope) { var itemLayout = lineData.getItemLayout(idx); if (!lineNeedsDraw(itemLayout)) { return; } var el = new lineDraw._ctor(lineData, idx, seriesScope); lineData.setItemGraphicEl(idx, el); lineDraw.group.add(el); } function doUpdate(lineDraw, oldLineData, newLineData, oldIdx, newIdx, seriesScope) { var itemEl = oldLineData.getItemGraphicEl(oldIdx); if (!lineNeedsDraw(newLineData.getItemLayout(newIdx))) { lineDraw.group.remove(itemEl); return; } if (!itemEl) { itemEl = new lineDraw._ctor(newLineData, newIdx, seriesScope); } else { itemEl.updateData(newLineData, newIdx, seriesScope); } newLineData.setItemGraphicEl(newIdx, itemEl); lineDraw.group.add(itemEl); } lineDrawProto.updateLayout = function () { var lineData = this._lineData; // Do not support update layout in incremental mode. if (!lineData) { return; } lineData.eachItemGraphicEl(function (el, idx) { el.updateLayout(lineData, idx); }, this); }; lineDrawProto.incrementalPrepareUpdate = function (lineData) { this._seriesScope = makeSeriesScope$1(lineData); this._lineData = null; this.group.removeAll(); }; function isEffectObject(el) { return el.animators && el.animators.length > 0; } lineDrawProto.incrementalUpdate = function (taskParams, lineData) { function updateIncrementalAndHover(el) { if (!el.isGroup && !isEffectObject(el)) { el.incremental = el.useHoverLayer = true; } } for (var idx = taskParams.start; idx < taskParams.end; idx++) { var itemLayout = lineData.getItemLayout(idx); if (lineNeedsDraw(itemLayout)) { var el = new this._ctor(lineData, idx, this._seriesScope); el.traverse(updateIncrementalAndHover); this.group.add(el); lineData.setItemGraphicEl(idx, el); } } }; function makeSeriesScope$1(lineData) { var hostModel = lineData.hostModel; return { lineStyle: hostModel.getModel('lineStyle').getLineStyle(), hoverLineStyle: hostModel.getModel('emphasis.lineStyle').getLineStyle(), labelModel: hostModel.getModel('label'), hoverLabelModel: hostModel.getModel('emphasis.label') }; } lineDrawProto.remove = function () { this._clearIncremental(); this._incremental = null; this.group.removeAll(); }; lineDrawProto._clearIncremental = function () { var incremental = this._incremental; if (incremental) { incremental.clearDisplaybles(); } }; function isPointNaN(pt) { return isNaN(pt[0]) || isNaN(pt[1]); } function lineNeedsDraw(pts) { return !isPointNaN(pts[0]) && !isPointNaN(pts[1]); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var markLineTransform = function (seriesModel, coordSys, mlModel, item) { var data = seriesModel.getData(); // Special type markLine like 'min', 'max', 'average', 'median' var mlType = item.type; if (!isArray(item) && ( mlType === 'min' || mlType === 'max' || mlType === 'average' || mlType === 'median' // In case // data: [{ // yAxis: 10 // }] || (item.xAxis != null || item.yAxis != null) ) ) { var valueAxis; var value; if (item.yAxis != null || item.xAxis != null) { valueAxis = coordSys.getAxis(item.yAxis != null ? 'y' : 'x'); value = retrieve(item.yAxis, item.xAxis); } else { var axisInfo = getAxisInfo$1(item, data, coordSys, seriesModel); valueAxis = axisInfo.valueAxis; var valueDataDim = getStackedDimension(data, axisInfo.valueDataDim); value = numCalculate(data, valueDataDim, mlType); } var valueIndex = valueAxis.dim === 'x' ? 0 : 1; var baseIndex = 1 - valueIndex; var mlFrom = clone(item); var mlTo = {}; mlFrom.type = null; mlFrom.coord = []; mlTo.coord = []; mlFrom.coord[baseIndex] = -Infinity; mlTo.coord[baseIndex] = Infinity; var precision = mlModel.get('precision'); if (precision >= 0 && typeof value === 'number') { value = +value.toFixed(Math.min(precision, 20)); } mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value; item = [mlFrom, mlTo, { // Extra option for tooltip and label type: mlType, valueIndex: item.valueIndex, // Force to use the value of calculated value. value: value }]; } item = [ dataTransform(seriesModel, item[0]), dataTransform(seriesModel, item[1]), extend({}, item[2]) ]; // Avoid line data type is extended by from(to) data type item[2].type = item[2].type || ''; // Merge from option and to option into line option merge(item[2], item[0]); merge(item[2], item[1]); return item; }; function isInifinity(val) { return !isNaN(val) && !isFinite(val); } // If a markLine has one dim function ifMarkLineHasOnlyDim(dimIndex, fromCoord, toCoord, coordSys) { var otherDimIndex = 1 - dimIndex; var dimName = coordSys.dimensions[dimIndex]; return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex]) && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]); } function markLineFilter(coordSys, item) { if (coordSys.type === 'cartesian2d') { var fromCoord = item[0].coord; var toCoord = item[1].coord; // In case // { // markLine: { // data: [{ yAxis: 2 }] // } // } if ( fromCoord && toCoord && (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys) || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys)) ) { return true; } } return dataFilter$1(coordSys, item[0]) && dataFilter$1(coordSys, item[1]); } function updateSingleMarkerEndLayout( data, idx, isFrom, seriesModel, api ) { var coordSys = seriesModel.coordinateSystem; var itemModel = data.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get('x'), api.getWidth()); var yPx = parsePercent$1(itemModel.get('y'), api.getHeight()); if (!isNaN(xPx) && !isNaN(yPx)) { point = [xPx, yPx]; } else { // Chart like bar may have there own marker positioning logic if (seriesModel.getMarkerPosition) { // Use the getMarkerPoisition point = seriesModel.getMarkerPosition( data.getValues(data.dimensions, idx) ); } else { var dims = coordSys.dimensions; var x = data.get(dims[0], idx); var y = data.get(dims[1], idx); point = coordSys.dataToPoint([x, y]); } // Expand line to the edge of grid if value on one axis is Inifnity // In case // markLine: { // data: [{ // yAxis: 2 // // or // type: 'average' // }] // } if (coordSys.type === 'cartesian2d') { var xAxis = coordSys.getAxis('x'); var yAxis = coordSys.getAxis('y'); var dims = coordSys.dimensions; if (isInifinity(data.get(dims[0], idx))) { point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]); } else if (isInifinity(data.get(dims[1], idx))) { point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]); } } // Use x, y if has any if (!isNaN(xPx)) { point[0] = xPx; } if (!isNaN(yPx)) { point[1] = yPx; } } data.setItemLayout(idx, point); } MarkerView.extend({ type: 'markLine', // updateLayout: function (markLineModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var mlModel = seriesModel.markLineModel; // if (mlModel) { // var mlData = mlModel.getData(); // var fromData = mlModel.__from; // var toData = mlModel.__to; // // Update visual and layout of from symbol and to symbol // fromData.each(function (idx) { // updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api); // updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api); // }); // // Update layout of line // mlData.each(function (idx) { // mlData.setItemLayout(idx, [ // fromData.getItemLayout(idx), // toData.getItemLayout(idx) // ]); // }); // this.markerGroupMap.get(seriesModel.id).updateLayout(); // } // }, this); // }, updateTransform: function (markLineModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var mlModel = seriesModel.markLineModel; if (mlModel) { var mlData = mlModel.getData(); var fromData = mlModel.__from; var toData = mlModel.__to; // Update visual and layout of from symbol and to symbol fromData.each(function (idx) { updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api); updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api); }); // Update layout of line mlData.each(function (idx) { mlData.setItemLayout(idx, [ fromData.getItemLayout(idx), toData.getItemLayout(idx) ]); }); this.markerGroupMap.get(seriesModel.id).updateLayout(); } }, this); }, renderSeries: function (seriesModel, mlModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesId = seriesModel.id; var seriesData = seriesModel.getData(); var lineDrawMap = this.markerGroupMap; var lineDraw = lineDrawMap.get(seriesId) || lineDrawMap.set(seriesId, new LineDraw()); this.group.add(lineDraw.group); var mlData = createList$2(coordSys, seriesModel, mlModel); var fromData = mlData.from; var toData = mlData.to; var lineData = mlData.line; mlModel.__from = fromData; mlModel.__to = toData; // Line data for tooltip and formatter mlModel.setData(lineData); var symbolType = mlModel.get('symbol'); var symbolSize = mlModel.get('symbolSize'); if (!isArray(symbolType)) { symbolType = [symbolType, symbolType]; } if (typeof symbolSize === 'number') { symbolSize = [symbolSize, symbolSize]; } // Update visual and layout of from symbol and to symbol mlData.from.each(function (idx) { updateDataVisualAndLayout(fromData, idx, true); updateDataVisualAndLayout(toData, idx, false); }); // Update visual and layout of line lineData.each(function (idx) { var lineColor = lineData.getItemModel(idx).get('lineStyle.color'); lineData.setItemVisual(idx, { color: lineColor || fromData.getItemVisual(idx, 'color') }); lineData.setItemLayout(idx, [ fromData.getItemLayout(idx), toData.getItemLayout(idx) ]); lineData.setItemVisual(idx, { 'fromSymbolRotate': fromData.getItemVisual(idx, 'symbolRotate'), 'fromSymbolSize': fromData.getItemVisual(idx, 'symbolSize'), 'fromSymbol': fromData.getItemVisual(idx, 'symbol'), 'toSymbolRotate': toData.getItemVisual(idx, 'symbolRotate'), 'toSymbolSize': toData.getItemVisual(idx, 'symbolSize'), 'toSymbol': toData.getItemVisual(idx, 'symbol') }); }); lineDraw.updateData(lineData); // Set host model for tooltip // FIXME mlData.line.eachItemGraphicEl(function (el, idx) { el.traverse(function (child) { child.dataModel = mlModel; }); }); function updateDataVisualAndLayout(data, idx, isFrom) { var itemModel = data.getItemModel(idx); updateSingleMarkerEndLayout( data, idx, isFrom, seriesModel, api ); data.setItemVisual(idx, { symbolRotate: itemModel.get('symbolRotate'), symbolSize: itemModel.get('symbolSize') || symbolSize[isFrom ? 0 : 1], symbol: itemModel.get('symbol', true) || symbolType[isFrom ? 0 : 1], color: itemModel.get('itemStyle.color') || seriesData.getVisual('color') }); } lineDraw.__keep = true; lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} coordSys * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$2(coordSys, seriesModel, mlModel) { var coordDimsInfos; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var info = seriesModel.getData().getDimensionInfo( seriesModel.getData().mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys return defaults({name: coordDim}, info); }); } else { coordDimsInfos = [{ name: 'value', type: 'float' }]; } var fromData = new List(coordDimsInfos, mlModel); var toData = new List(coordDimsInfos, mlModel); // No dimensions var lineData = new List([], mlModel); var optData = map(mlModel.get('data'), curry( markLineTransform, seriesModel, coordSys, mlModel )); if (coordSys) { optData = filter( optData, curry(markLineFilter, coordSys) ); } var dimValueGetter$$1 = coordSys ? dimValueGetter : function (item) { return item.value; }; fromData.initData( map(optData, function (item) { return item[0]; }), null, dimValueGetter$$1 ); toData.initData( map(optData, function (item) { return item[1]; }), null, dimValueGetter$$1 ); lineData.initData( map(optData, function (item) { return item[2]; }) ); lineData.hasItemOption = true; return { from: fromData, to: toData, line: lineData }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ registerPreprocessor(function (opt) { // Make sure markLine component is enabled opt.markLine = opt.markLine || {}; }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ MarkerModel.extend({ type: 'markArea', defaultOption: { zlevel: 0, // PENDING z: 1, tooltip: { trigger: 'item' }, // markArea should fixed on the coordinate system animation: false, label: { show: true, position: 'top' }, itemStyle: { // color and borderColor default to use color from series // color: 'auto' // borderColor: 'auto' borderWidth: 0 }, emphasis: { label: { show: true, position: 'top' } } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // TODO Better on polar var markAreaTransform = function (seriesModel, coordSys, maModel, item) { var lt = dataTransform(seriesModel, item[0]); var rb = dataTransform(seriesModel, item[1]); var retrieve$$1 = retrieve; // FIXME make sure lt is less than rb var ltCoord = lt.coord; var rbCoord = rb.coord; ltCoord[0] = retrieve$$1(ltCoord[0], -Infinity); ltCoord[1] = retrieve$$1(ltCoord[1], -Infinity); rbCoord[0] = retrieve$$1(rbCoord[0], Infinity); rbCoord[1] = retrieve$$1(rbCoord[1], Infinity); // Merge option into one var result = mergeAll([{}, lt, rb]); result.coord = [ lt.coord, rb.coord ]; result.x0 = lt.x; result.y0 = lt.y; result.x1 = rb.x; result.y1 = rb.y; return result; }; function isInifinity$1(val) { return !isNaN(val) && !isFinite(val); } // If a markArea has one dim function ifMarkLineHasOnlyDim$1(dimIndex, fromCoord, toCoord, coordSys) { var otherDimIndex = 1 - dimIndex; return isInifinity$1(fromCoord[otherDimIndex]) && isInifinity$1(toCoord[otherDimIndex]); } function markAreaFilter(coordSys, item) { var fromCoord = item.coord[0]; var toCoord = item.coord[1]; if (coordSys.type === 'cartesian2d') { // In case // { // markArea: { // data: [{ yAxis: 2 }] // } // } if ( fromCoord && toCoord && (ifMarkLineHasOnlyDim$1(1, fromCoord, toCoord, coordSys) || ifMarkLineHasOnlyDim$1(0, fromCoord, toCoord, coordSys)) ) { return true; } } return dataFilter$1(coordSys, { coord: fromCoord, x: item.x0, y: item.y0 }) || dataFilter$1(coordSys, { coord: toCoord, x: item.x1, y: item.y1 }); } // dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0'] function getSingleMarkerEndPoint(data, idx, dims, seriesModel, api) { var coordSys = seriesModel.coordinateSystem; var itemModel = data.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get(dims[0]), api.getWidth()); var yPx = parsePercent$1(itemModel.get(dims[1]), api.getHeight()); if (!isNaN(xPx) && !isNaN(yPx)) { point = [xPx, yPx]; } else { // Chart like bar may have there own marker positioning logic if (seriesModel.getMarkerPosition) { // Use the getMarkerPoisition point = seriesModel.getMarkerPosition( data.getValues(dims, idx) ); } else { var x = data.get(dims[0], idx); var y = data.get(dims[1], idx); var pt = [x, y]; coordSys.clampData && coordSys.clampData(pt, pt); point = coordSys.dataToPoint(pt, true); } if (coordSys.type === 'cartesian2d') { var xAxis = coordSys.getAxis('x'); var yAxis = coordSys.getAxis('y'); var x = data.get(dims[0], idx); var y = data.get(dims[1], idx); if (isInifinity$1(x)) { point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[dims[0] === 'x0' ? 0 : 1]); } else if (isInifinity$1(y)) { point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[dims[1] === 'y0' ? 0 : 1]); } } // Use x, y if has any if (!isNaN(xPx)) { point[0] = xPx; } if (!isNaN(yPx)) { point[1] = yPx; } } return point; } var dimPermutations = [['x0', 'y0'], ['x1', 'y0'], ['x1', 'y1'], ['x0', 'y1']]; MarkerView.extend({ type: 'markArea', // updateLayout: function (markAreaModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var maModel = seriesModel.markAreaModel; // if (maModel) { // var areaData = maModel.getData(); // areaData.each(function (idx) { // var points = zrUtil.map(dimPermutations, function (dim) { // return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); // }); // // Layout // areaData.setItemLayout(idx, points); // var el = areaData.getItemGraphicEl(idx); // el.setShape('points', points); // }); // } // }, this); // }, updateTransform: function (markAreaModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var maModel = seriesModel.markAreaModel; if (maModel) { var areaData = maModel.getData(); areaData.each(function (idx) { var points = map(dimPermutations, function (dim) { return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); }); // Layout areaData.setItemLayout(idx, points); var el = areaData.getItemGraphicEl(idx); el.setShape('points', points); }); } }, this); }, renderSeries: function (seriesModel, maModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesId = seriesModel.id; var seriesData = seriesModel.getData(); var areaGroupMap = this.markerGroupMap; var polygonGroup = areaGroupMap.get(seriesId) || areaGroupMap.set(seriesId, {group: new Group()}); this.group.add(polygonGroup.group); polygonGroup.__keep = true; var areaData = createList$3(coordSys, seriesModel, maModel); // Line data for tooltip and formatter maModel.setData(areaData); // Update visual and layout of line areaData.each(function (idx) { // Layout var points = map(dimPermutations, function (dim) { return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); }); // If none of the area is inside coordSys, allClipped is set to be true // in layout so that label will not be displayed. See #12591 var allClipped = true; each$1(dimPermutations, function (dim) { if (!allClipped) { return; } var xValue = areaData.get(dim[0], idx); var yValue = areaData.get(dim[1], idx); // If is infinity, the axis should be considered not clipped if ((isInifinity$1(xValue) || coordSys.getAxis('x').containData(xValue)) && (isInifinity$1(yValue) || coordSys.getAxis('y').containData(yValue)) ) { allClipped = false; } }); areaData.setItemLayout(idx, { points: points, allClipped: allClipped }); // Visual areaData.setItemVisual(idx, { color: seriesData.getVisual('color') }); }); areaData.diff(polygonGroup.__data) .add(function (idx) { var layout = areaData.getItemLayout(idx); if (!layout.allClipped) { var polygon = new Polygon({ shape: { points: layout.points } }); areaData.setItemGraphicEl(idx, polygon); polygonGroup.group.add(polygon); } }) .update(function (newIdx, oldIdx) { var polygon = polygonGroup.__data.getItemGraphicEl(oldIdx); var layout = areaData.getItemLayout(newIdx); if (!layout.allClipped) { if (polygon) { updateProps(polygon, { shape: { points: layout.points } }, maModel, newIdx); } else { polygon = new Polygon({ shape: { points: layout.points } }); } areaData.setItemGraphicEl(newIdx, polygon); polygonGroup.group.add(polygon); } else if (polygon) { polygonGroup.group.remove(polygon); } }) .remove(function (idx) { var polygon = polygonGroup.__data.getItemGraphicEl(idx); polygonGroup.group.remove(polygon); }) .execute(); areaData.eachItemGraphicEl(function (polygon, idx) { var itemModel = areaData.getItemModel(idx); var labelModel = itemModel.getModel('label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var color = areaData.getItemVisual(idx, 'color'); polygon.useStyle( defaults( itemModel.getModel('itemStyle').getItemStyle(), { fill: modifyAlpha(color, 0.4), stroke: color } ) ); polygon.hoverStyle = itemModel.getModel('emphasis.itemStyle').getItemStyle(); setLabelStyle( polygon.style, polygon.hoverStyle, labelModel, labelHoverModel, { labelFetcher: maModel, labelDataIndex: idx, defaultText: areaData.getName(idx) || '', isRectText: true, autoColor: color } ); setHoverStyle(polygon, {}); polygon.dataModel = maModel; }); polygonGroup.__data = areaData; polygonGroup.group.silent = maModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} coordSys * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$3(coordSys, seriesModel, maModel) { var coordDimsInfos; var areaData; var dims = ['x0', 'y0', 'x1', 'y1']; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var data = seriesModel.getData(); var info = data.getDimensionInfo( data.mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys return defaults({name: coordDim}, info); }); areaData = new List(map(dims, function (dim, idx) { return { name: dim, type: coordDimsInfos[idx % 2].type }; }), maModel); } else { coordDimsInfos = [{ name: 'value', type: 'float' }]; areaData = new List(coordDimsInfos, maModel); } var optData = map(maModel.get('data'), curry( markAreaTransform, seriesModel, coordSys, maModel )); if (coordSys) { optData = filter( optData, curry(markAreaFilter, coordSys) ); } var dimValueGetter$$1 = coordSys ? function (item, dimName, dataIndex, dimIndex) { return item.coord[Math.floor(dimIndex / 2)][dimIndex % 2]; } : function (item) { return item.value; }; areaData.initData(optData, null, dimValueGetter$$1); areaData.hasItemOption = true; return areaData; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ registerPreprocessor(function (opt) { // Make sure markArea component is enabled opt.markArea = opt.markArea || {}; }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ComponentModel.registerSubTypeDefaulter('dataZoom', function () { // Default 'slider' when no type specified. return 'slider'; }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var AXIS_DIMS = ['x', 'y', 'z', 'radius', 'angle', 'single']; // Supported coords. var COORDS = ['cartesian2d', 'polar', 'singleAxis']; /** * @param {string} coordType * @return {boolean} */ function isCoordSupported(coordType) { return indexOf(COORDS, coordType) >= 0; } /** * Create "each" method to iterate names. * * @pubilc * @param {Array.} names * @param {Array.=} attrs * @return {Function} */ function createNameEach(names, attrs) { names = names.slice(); var capitalNames = map(names, capitalFirst); attrs = (attrs || []).slice(); var capitalAttrs = map(attrs, capitalFirst); return function (callback, context) { each$1(names, function (name, index) { var nameObj = {name: name, capital: capitalNames[index]}; for (var j = 0; j < attrs.length; j++) { nameObj[attrs[j]] = name + capitalAttrs[j]; } callback.call(context, nameObj); }); }; } /** * Iterate each dimension name. * * @public * @param {Function} callback The parameter is like: * { * name: 'angle', * capital: 'Angle', * axis: 'angleAxis', * axisIndex: 'angleAixs', * index: 'angleIndex' * } * @param {Object} context */ var eachAxisDim$1 = createNameEach(AXIS_DIMS, ['axisIndex', 'axis', 'index', 'id']); /** * If tow dataZoomModels has the same axis controlled, we say that they are 'linked'. * dataZoomModels and 'links' make up one or more graphics. * This function finds the graphic where the source dataZoomModel is in. * * @public * @param {Function} forEachNode Node iterator. * @param {Function} forEachEdgeType edgeType iterator * @param {Function} edgeIdGetter Giving node and edgeType, return an array of edge id. * @return {Function} Input: sourceNode, Output: Like {nodes: [], dims: {}} */ function createLinkedNodesFinder(forEachNode, forEachEdgeType, edgeIdGetter) { return function (sourceNode) { var result = { nodes: [], records: {} // key: edgeType.name, value: Object (key: edge id, value: boolean). }; forEachEdgeType(function (edgeType) { result.records[edgeType.name] = {}; }); if (!sourceNode) { return result; } absorb(sourceNode, result); var existsLink; do { existsLink = false; forEachNode(processSingleNode); } while (existsLink); function processSingleNode(node) { if (!isNodeAbsorded(node, result) && isLinked(node, result)) { absorb(node, result); existsLink = true; } } return result; }; function isNodeAbsorded(node, result) { return indexOf(result.nodes, node) >= 0; } function isLinked(node, result) { var hasLink = false; forEachEdgeType(function (edgeType) { each$1(edgeIdGetter(node, edgeType) || [], function (edgeId) { result.records[edgeType.name][edgeId] && (hasLink = true); }); }); return hasLink; } function absorb(node, result) { result.nodes.push(node); forEachEdgeType(function (edgeType) { each$1(edgeIdGetter(node, edgeType) || [], function (edgeId) { result.records[edgeType.name][edgeId] = true; }); }); } } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Calculate slider move result. * Usage: * (1) If both handle0 and handle1 are needed to be moved, set minSpan the same as * maxSpan and the same as `Math.abs(handleEnd[1] - handleEnds[0])`. * (2) If handle0 is forbidden to cross handle1, set minSpan as `0`. * * @param {number} delta Move length. * @param {Array.} handleEnds handleEnds[0] can be bigger then handleEnds[1]. * handleEnds will be modified in this method. * @param {Array.} extent handleEnds is restricted by extent. * extent[0] should less or equals than extent[1]. * @param {number|string} handleIndex Can be 'all', means that both move the two handleEnds. * @param {number} [minSpan] The range of dataZoom can not be smaller than that. * If not set, handle0 and cross handle1. If set as a non-negative * number (including `0`), handles will push each other when reaching * the minSpan. * @param {number} [maxSpan] The range of dataZoom can not be larger than that. * @return {Array.} The input handleEnds. */ var sliderMove = function (delta, handleEnds, extent, handleIndex, minSpan, maxSpan) { delta = delta || 0; var extentSpan = extent[1] - extent[0]; // Notice maxSpan and minSpan can be null/undefined. if (minSpan != null) { minSpan = restrict(minSpan, [0, extentSpan]); } if (maxSpan != null) { maxSpan = Math.max(maxSpan, minSpan != null ? minSpan : 0); } if (handleIndex === 'all') { var handleSpan = Math.abs(handleEnds[1] - handleEnds[0]); handleSpan = restrict(handleSpan, [0, extentSpan]); minSpan = maxSpan = restrict(handleSpan, [minSpan, maxSpan]); handleIndex = 0; } handleEnds[0] = restrict(handleEnds[0], extent); handleEnds[1] = restrict(handleEnds[1], extent); var originalDistSign = getSpanSign(handleEnds, handleIndex); handleEnds[handleIndex] += delta; // Restrict in extent. var extentMinSpan = minSpan || 0; var realExtent = extent.slice(); originalDistSign.sign < 0 ? (realExtent[0] += extentMinSpan) : (realExtent[1] -= extentMinSpan); handleEnds[handleIndex] = restrict(handleEnds[handleIndex], realExtent); // Expand span. var currDistSign = getSpanSign(handleEnds, handleIndex); if (minSpan != null && ( currDistSign.sign !== originalDistSign.sign || currDistSign.span < minSpan )) { // If minSpan exists, 'cross' is forbidden. handleEnds[1 - handleIndex] = handleEnds[handleIndex] + originalDistSign.sign * minSpan; } // Shrink span. var currDistSign = getSpanSign(handleEnds, handleIndex); if (maxSpan != null && currDistSign.span > maxSpan) { handleEnds[1 - handleIndex] = handleEnds[handleIndex] + currDistSign.sign * maxSpan; } return handleEnds; }; function getSpanSign(handleEnds, handleIndex) { var dist = handleEnds[handleIndex] - handleEnds[1 - handleIndex]; // If `handleEnds[0] === handleEnds[1]`, always believe that handleEnd[0] // is at left of handleEnds[1] for non-cross case. return {span: Math.abs(dist), sign: dist > 0 ? -1 : dist < 0 ? 1 : handleIndex ? -1 : 1}; } function restrict(value, extend) { return Math.min( extend[1] != null ? extend[1] : Infinity, Math.max(extend[0] != null ? extend[0] : -Infinity, value) ); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$13 = each$1; var asc$1 = asc; /** * Operate single axis. * One axis can only operated by one axis operator. * Different dataZoomModels may be defined to operate the same axis. * (i.e. 'inside' data zoom and 'slider' data zoom components) * So dataZoomModels share one axisProxy in that case. * * @class */ var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) { /** * @private * @type {string} */ this._dimName = dimName; /** * @private */ this._axisIndex = axisIndex; /** * @private * @type {Array.} */ this._valueWindow; /** * @private * @type {Array.} */ this._percentWindow; /** * @private * @type {Array.} */ this._dataExtent; /** * {minSpan, maxSpan, minValueSpan, maxValueSpan} * @private * @type {Object} */ this._minMaxSpan; /** * @readOnly * @type {module: echarts/model/Global} */ this.ecModel = ecModel; /** * @private * @type {module: echarts/component/dataZoom/DataZoomModel} */ this._dataZoomModel = dataZoomModel; // /** // * @readOnly // * @private // */ // this.hasSeriesStacked; }; AxisProxy.prototype = { constructor: AxisProxy, /** * Whether the axisProxy is hosted by dataZoomModel. * * @public * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel * @return {boolean} */ hostedBy: function (dataZoomModel) { return this._dataZoomModel === dataZoomModel; }, /** * @return {Array.} Value can only be NaN or finite value. */ getDataValueWindow: function () { return this._valueWindow.slice(); }, /** * @return {Array.} */ getDataPercentWindow: function () { return this._percentWindow.slice(); }, /** * @public * @param {number} axisIndex * @return {Array} seriesModels */ getTargetSeriesModels: function () { var seriesModels = []; var ecModel = this.ecModel; ecModel.eachSeries(function (seriesModel) { if (isCoordSupported(seriesModel.get('coordinateSystem'))) { var dimName = this._dimName; var axisModel = ecModel.queryComponents({ mainType: dimName + 'Axis', index: seriesModel.get(dimName + 'AxisIndex'), id: seriesModel.get(dimName + 'AxisId') })[0]; if (this._axisIndex === (axisModel && axisModel.componentIndex)) { seriesModels.push(seriesModel); } } }, this); return seriesModels; }, getAxisModel: function () { return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex); }, getOtherAxisModel: function () { var axisDim = this._dimName; var ecModel = this.ecModel; var axisModel = this.getAxisModel(); var isCartesian = axisDim === 'x' || axisDim === 'y'; var otherAxisDim; var coordSysIndexName; if (isCartesian) { coordSysIndexName = 'gridIndex'; otherAxisDim = axisDim === 'x' ? 'y' : 'x'; } else { coordSysIndexName = 'polarIndex'; otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle'; } var foundOtherAxisModel; ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) { if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0) ) { foundOtherAxisModel = otherAxisModel; } }); return foundOtherAxisModel; }, getMinMaxSpan: function () { return clone(this._minMaxSpan); }, /** * Only calculate by given range and this._dataExtent, do not change anything. * * @param {Object} opt * @param {number} [opt.start] * @param {number} [opt.end] * @param {number} [opt.startValue] * @param {number} [opt.endValue] */ calculateDataWindow: function (opt) { var dataExtent = this._dataExtent; var axisModel = this.getAxisModel(); var scale = axisModel.axis.scale; var rangePropMode = this._dataZoomModel.getRangePropMode(); var percentExtent = [0, 100]; var percentWindow = []; var valueWindow = []; var hasPropModeValue; each$13(['start', 'end'], function (prop, idx) { var boundPercent = opt[prop]; var boundValue = opt[prop + 'Value']; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or // on `valueProp` ('startValue', 'endValue'). (They are based on the data extent // but not min/max of axis, which will be calculated by data window then). // The former one is suitable for cases that a dataZoom component controls multiple // axes with different unit or extent, and the latter one is suitable for accurate // zoom by pixel (e.g., in dataZoomSelect). // we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated // only when setOption or dispatchAction, otherwise it remains its original value. // (Why not only record `percentProp` and always map to `valueProp`? Because // the map `valueProp` -> `percentProp` -> `valueProp` probably not the original // `valueProp`. consider two axes constrolled by one dataZoom. They have different // data extent. All of values that are overflow the `dataExtent` will be calculated // to percent '100%'). if (rangePropMode[idx] === 'percent') { boundPercent == null && (boundPercent = percentExtent[idx]); // Use scale.parse to math round for category or time axis. boundValue = scale.parse(linearMap( boundPercent, percentExtent, dataExtent )); } else { hasPropModeValue = true; boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue); // Calculating `percent` from `value` may be not accurate, because // This calculation can not be inversed, because all of values that // are overflow the `dataExtent` will be calculated to percent '100%' boundPercent = linearMap( boundValue, dataExtent, percentExtent ); } // valueWindow[idx] = round(boundValue); // percentWindow[idx] = round(boundPercent); valueWindow[idx] = boundValue; percentWindow[idx] = boundPercent; }); asc$1(valueWindow); asc$1(percentWindow); // The windows from user calling of `dispatchAction` might be out of the extent, // or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we dont restrict window // by `zoomLock` here, because we see `zoomLock` just as a interaction constraint, // where API is able to initialize/modify the window size even though `zoomLock` // specified. var spans = this._minMaxSpan; hasPropModeValue ? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false) : restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true); function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) { var suffix = toValue ? 'Span' : 'ValueSpan'; sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]); for (var i = 0; i < 2; i++) { toWindow[i] = linearMap(fromWindow[i], fromExtent, toExtent, true); toValue && (toWindow[i] = scale.parse(toWindow[i])); } } return { valueWindow: valueWindow, percentWindow: percentWindow }; }, /** * Notice: reset should not be called before series.restoreData() called, * so it is recommanded to be called in "process stage" but not "model init * stage". * * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ reset: function (dataZoomModel) { if (dataZoomModel !== this._dataZoomModel) { return; } var targetSeries = this.getTargetSeriesModels(); // Culculate data window and data extent, and record them. this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); // this.hasSeriesStacked = false; // each(targetSeries, function (series) { // var data = series.getData(); // var dataDim = data.mapDimension(this._dimName); // var stackedDimension = data.getCalculationInfo('stackedDimension'); // if (stackedDimension && stackedDimension === dataDim) { // this.hasSeriesStacked = true; // } // }, this); // `calculateDataWindow` uses min/maxSpan. setMinMaxSpan(this); var dataWindow = this.calculateDataWindow(dataZoomModel.settledOption); this._valueWindow = dataWindow.valueWindow; this._percentWindow = dataWindow.percentWindow; // Update axis setting then. setAxisModel(this); }, /** * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ restore: function (dataZoomModel) { if (dataZoomModel !== this._dataZoomModel) { return; } this._valueWindow = this._percentWindow = null; setAxisModel(this, true); }, /** * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ filterData: function (dataZoomModel, api) { if (dataZoomModel !== this._dataZoomModel) { return; } var axisDim = this._dimName; var seriesModels = this.getTargetSeriesModels(); var filterMode = dataZoomModel.get('filterMode'); var valueWindow = this._valueWindow; if (filterMode === 'none') { return; } // FIXME // Toolbox may has dataZoom injected. And if there are stacked bar chart // with NaN data, NaN will be filtered and stack will be wrong. // So we need to force the mode to be set empty. // In fect, it is not a big deal that do not support filterMode-'filter' // when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis // selection" some day, which might need "adapt to data extent on the // otherAxis", which is disabled by filterMode-'empty'. // But currently, stack has been fixed to based on value but not index, // so this is not an issue any more. // var otherAxisModel = this.getOtherAxisModel(); // if (dataZoomModel.get('$fromToolbox') // && otherAxisModel // && otherAxisModel.hasSeriesStacked // ) { // filterMode = 'empty'; // } // TODO // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet. each$13(seriesModels, function (seriesModel) { var seriesData = seriesModel.getData(); var dataDims = seriesData.mapDimension(axisDim, true); if (!dataDims.length) { return; } if (filterMode === 'weakFilter') { seriesData.filterSelf(function (dataIndex) { var leftOut; var rightOut; var hasValue; for (var i = 0; i < dataDims.length; i++) { var value = seriesData.get(dataDims[i], dataIndex); var thisHasValue = !isNaN(value); var thisLeftOut = value < valueWindow[0]; var thisRightOut = value > valueWindow[1]; if (thisHasValue && !thisLeftOut && !thisRightOut) { return true; } thisHasValue && (hasValue = true); thisLeftOut && (leftOut = true); thisRightOut && (rightOut = true); } // If both left out and right out, do not filter. return hasValue && leftOut && rightOut; }); } else { each$13(dataDims, function (dim) { if (filterMode === 'empty') { seriesModel.setData( seriesData = seriesData.map(dim, function (value) { return !isInWindow(value) ? NaN : value; }) ); } else { var range = {}; range[dim] = valueWindow; // console.time('select'); seriesData.selectRange(range); // console.timeEnd('select'); } }); } each$13(dataDims, function (dim) { seriesData.setApproximateExtent(valueWindow, dim); }); }); function isInWindow(value) { return value >= valueWindow[0] && value <= valueWindow[1]; } } }; function calculateDataExtent(axisProxy, axisDim, seriesModels) { var dataExtent = [Infinity, -Infinity]; each$13(seriesModels, function (seriesModel) { var seriesData = seriesModel.getData(); if (seriesData) { each$13(seriesData.mapDimension(axisDim, true), function (dim) { var seriesExtent = seriesData.getApproximateExtent(dim); seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]); seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]); }); } }); if (dataExtent[1] < dataExtent[0]) { dataExtent = [NaN, NaN]; } // It is important to get "consistent" extent when more then one axes is // controlled by a `dataZoom`, otherwise those axes will not be synchronized // when zooming. But it is difficult to know what is "consistent", considering // axes have different type or even different meanings (For example, two // time axes are used to compare data of the same date in different years). // So basically dataZoom just obtains extent by series.data (in category axis // extent can be obtained from axis.data). // Nevertheless, user can set min/max/scale on axes to make extent of axes // consistent. fixExtentByAxis(axisProxy, dataExtent); return dataExtent; } function fixExtentByAxis(axisProxy, dataExtent) { var axisModel = axisProxy.getAxisModel(); var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined // by axis.data by default. var isCategoryAxis = axisModel.get('type') === 'category'; var axisDataLen = isCategoryAxis && axisModel.getCategories().length; if (min != null && min !== 'dataMin' && typeof min !== 'function') { dataExtent[0] = min; } else if (isCategoryAxis) { dataExtent[0] = axisDataLen > 0 ? 0 : NaN; } var max = axisModel.getMax(true); if (max != null && max !== 'dataMax' && typeof max !== 'function') { dataExtent[1] = max; } else if (isCategoryAxis) { dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN; } if (!axisModel.get('scale', true)) { dataExtent[0] > 0 && (dataExtent[0] = 0); dataExtent[1] < 0 && (dataExtent[1] = 0); } // For value axis, if min/max/scale are not set, we just use the extent obtained // by series data, which may be a little different from the extent calculated by // `axisHelper.getScaleExtent`. But the different just affects the experience a // little when zooming. So it will not be fixed until some users require it strongly. return dataExtent; } function setAxisModel(axisProxy, isRestore) { var axisModel = axisProxy.getAxisModel(); var percentWindow = axisProxy._percentWindow; var valueWindow = axisProxy._valueWindow; if (!percentWindow) { return; } // [0, 500]: arbitrary value, guess axis extent. var precision = getPixelPrecision(valueWindow, [0, 500]); precision = Math.min(precision, 20); // isRestore or isFull var useOrigin = isRestore || (percentWindow[0] === 0 && percentWindow[1] === 100); axisModel.setRange( useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision) ); } function setMinMaxSpan(axisProxy) { var minMaxSpan = axisProxy._minMaxSpan = {}; var dataZoomModel = axisProxy._dataZoomModel; var dataExtent = axisProxy._dataExtent; each$13(['min', 'max'], function (minMax) { var percentSpan = dataZoomModel.get(minMax + 'Span'); var valueSpan = dataZoomModel.get(minMax + 'ValueSpan'); valueSpan != null && (valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan)); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan if (valueSpan != null) { percentSpan = linearMap( dataExtent[0] + valueSpan, dataExtent, [0, 100], true ); } else if (percentSpan != null) { valueSpan = linearMap( percentSpan, [0, 100], dataExtent, true ) - dataExtent[0]; } minMaxSpan[minMax + 'Span'] = percentSpan; minMaxSpan[minMax + 'ValueSpan'] = valueSpan; }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$12 = each$1; var eachAxisDim = eachAxisDim$1; var DataZoomModel = extendComponentModel({ type: 'dataZoom', dependencies: [ 'xAxis', 'yAxis', 'zAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series' ], /** * @protected */ defaultOption: { zlevel: 0, z: 4, // Higher than normal component (z: 2). orient: null, // Default auto by axisIndex. Possible value: 'horizontal', 'vertical'. xAxisIndex: null, // Default the first horizontal category axis. yAxisIndex: null, // Default the first vertical category axis. filterMode: 'filter', // Possible values: 'filter' or 'empty' or 'weakFilter'. // 'filter': data items which are out of window will be removed. This option is // applicable when filtering outliers. For each data item, it will be // filtered if one of the relevant dimensions is out of the window. // 'weakFilter': data items which are out of window will be removed. This option // is applicable when filtering outliers. For each data item, it will be // filtered only if all of the relevant dimensions are out of the same // side of the window. // 'empty': data items which are out of window will be set to empty. // This option is applicable when user should not neglect // that there are some data items out of window. // 'none': Do not filter. // Taking line chart as an example, line will be broken in // the filtered points when filterModel is set to 'empty', but // be connected when set to 'filter'. throttle: null, // Dispatch action by the fixed rate, avoid frequency. // default 100. Do not throttle when use null/undefined. // If animation === true and animationDurationUpdate > 0, // default value is 100, otherwise 20. start: 0, // Start percent. 0 ~ 100 end: 100, // End percent. 0 ~ 100 startValue: null, // Start value. If startValue specified, start is ignored. endValue: null, // End value. If endValue specified, end is ignored. minSpan: null, // 0 ~ 100 maxSpan: null, // 0 ~ 100 minValueSpan: null, // The range of dataZoom can not be smaller than that. maxValueSpan: null, // The range of dataZoom can not be larger than that. rangeMode: null // Array, can be 'value' or 'percent'. }, /** * @override */ init: function (option, parentModel, ecModel) { /** * key like x_0, y_1 * @private * @type {Object} */ this._dataIntervalByAxis = {}; /** * @private */ this._dataInfo = {}; /** * key like x_0, y_1 * @private */ this._axisProxies = {}; /** * @readOnly */ this.textStyleModel; /** * @private */ this._autoThrottle = true; /** * It is `[rangeModeForMin, rangeModeForMax]`. * The optional values for `rangeMode`: * + `'value'` mode: the axis extent will always be determined by * `dataZoom.startValue` and `dataZoom.endValue`, despite * how data like and how `axis.min` and `axis.max` are. * + `'percent'` mode: `100` represents 100% of the `[dMin, dMax]`, * where `dMin` is `axis.min` if `axis.min` specified, otherwise `data.extent[0]`, * and `dMax` is `axis.max` if `axis.max` specified, otherwise `data.extent[1]`. * Axis extent will be determined by the result of the percent of `[dMin, dMax]`. * * For example, when users are using dynamic data (update data periodically via `setOption`), * if in `'value`' mode, the window will be kept in a fixed value range despite how * data are appended, while if in `'percent'` mode, whe window range will be changed alone with * the appended data (suppose `axis.min` and `axis.max` are not specified). * * @private */ this._rangePropMode = ['percent', 'percent']; var inputRawOption = retrieveRawOption(option); /** * Suppose a "main process" start at the point that model prepared (that is, * model initialized or merged or method called in `action`). * We should keep the `main process` idempotent, that is, given a set of values * on `option`, we get the same result. * * But sometimes, values on `option` will be updated for providing users * a "final calculated value" (`dataZoomProcessor` will do that). Those value * should not be the base/input of the `main process`. * * So in that case we should save and keep the input of the `main process` * separately, called `settledOption`. * * For example, consider the case: * (Step_1) brush zoom the grid by `toolbox.dataZoom`, * where the original input `option.startValue`, `option.endValue` are earsed by * calculated value. * (Step)2) click the legend to hide and show a series, * where the new range is calculated by the earsed `startValue` and `endValue`, * which brings incorrect result. * * @readOnly */ this.settledOption = inputRawOption; this.mergeDefaultAndTheme(option, ecModel); this.doInit(inputRawOption); }, /** * @override */ mergeOption: function (newOption) { var inputRawOption = retrieveRawOption(newOption); //FIX #2591 merge(this.option, newOption, true); merge(this.settledOption, inputRawOption, true); this.doInit(inputRawOption); }, /** * @protected */ doInit: function (inputRawOption) { var thisOption = this.option; // Disable realtime view update if canvas is not supported. if (!env$1.canvasSupported) { thisOption.realtime = false; } this._setDefaultThrottle(inputRawOption); updateRangeUse(this, inputRawOption); var settledOption = this.settledOption; each$12([['start', 'startValue'], ['end', 'endValue']], function (names, index) { // start/end has higher priority over startValue/endValue if they // both set, but we should make chart.setOption({endValue: 1000}) // effective, rather than chart.setOption({endValue: 1000, end: null}). if (this._rangePropMode[index] === 'value') { thisOption[names[0]] = settledOption[names[0]] = null; } // Otherwise do nothing and use the merge result. }, this); this.textStyleModel = this.getModel('textStyle'); this._resetTarget(); this._giveAxisProxies(); }, /** * @private */ _giveAxisProxies: function () { var axisProxies = this._axisProxies; this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) { var axisModel = this.dependentModels[dimNames.axis][axisIndex]; // If exists, share axisProxy with other dataZoomModels. var axisProxy = axisModel.__dzAxisProxy || ( // Use the first dataZoomModel as the main model of axisProxy. axisModel.__dzAxisProxy = new AxisProxy( dimNames.name, axisIndex, this, ecModel ) ); // FIXME // dispose __dzAxisProxy axisProxies[dimNames.name + '_' + axisIndex] = axisProxy; }, this); }, /** * @private */ _resetTarget: function () { var thisOption = this.option; var autoMode = this._judgeAutoMode(); eachAxisDim(function (dimNames) { var axisIndexName = dimNames.axisIndex; thisOption[axisIndexName] = normalizeToArray( thisOption[axisIndexName] ); }, this); if (autoMode === 'axisIndex') { this._autoSetAxisIndex(); } else if (autoMode === 'orient') { this._autoSetOrient(); } }, /** * @private */ _judgeAutoMode: function () { // Auto set only works for setOption at the first time. // The following is user's reponsibility. So using merged // option is OK. var thisOption = this.option; var hasIndexSpecified = false; eachAxisDim(function (dimNames) { // When user set axisIndex as a empty array, we think that user specify axisIndex // but do not want use auto mode. Because empty array may be encountered when // some error occured. if (thisOption[dimNames.axisIndex] != null) { hasIndexSpecified = true; } }, this); var orient = thisOption.orient; if (orient == null && hasIndexSpecified) { return 'orient'; } else if (!hasIndexSpecified) { if (orient == null) { thisOption.orient = 'horizontal'; } return 'axisIndex'; } }, /** * @private */ _autoSetAxisIndex: function () { var autoAxisIndex = true; var orient = this.get('orient', true); var thisOption = this.option; var dependentModels = this.dependentModels; if (autoAxisIndex) { // Find axis that parallel to dataZoom as default. var dimName = orient === 'vertical' ? 'y' : 'x'; if (dependentModels[dimName + 'Axis'].length) { thisOption[dimName + 'AxisIndex'] = [0]; autoAxisIndex = false; } else { each$12(dependentModels.singleAxis, function (singleAxisModel) { if (autoAxisIndex && singleAxisModel.get('orient', true) === orient) { thisOption.singleAxisIndex = [singleAxisModel.componentIndex]; autoAxisIndex = false; } }); } } if (autoAxisIndex) { // Find the first category axis as default. (consider polar) eachAxisDim(function (dimNames) { if (!autoAxisIndex) { return; } var axisIndices = []; var axisModels = this.dependentModels[dimNames.axis]; if (axisModels.length && !axisIndices.length) { for (var i = 0, len = axisModels.length; i < len; i++) { if (axisModels[i].get('type') === 'category') { axisIndices.push(i); } } } thisOption[dimNames.axisIndex] = axisIndices; if (axisIndices.length) { autoAxisIndex = false; } }, this); } if (autoAxisIndex) { // FIXME // 这里是兼容ec2的写法(没指定xAxisIndex和yAxisIndex时把scatter和双数值轴折柱纳入dataZoom控制), // 但是实际是否需要Grid.js#getScaleByOption来判断(考虑time,log等axis type)? // If both dataZoom.xAxisIndex and dataZoom.yAxisIndex is not specified, // dataZoom component auto adopts series that reference to // both xAxis and yAxis which type is 'value'. this.ecModel.eachSeries(function (seriesModel) { if (this._isSeriesHasAllAxesTypeOf(seriesModel, 'value')) { eachAxisDim(function (dimNames) { var axisIndices = thisOption[dimNames.axisIndex]; var axisIndex = seriesModel.get(dimNames.axisIndex); var axisId = seriesModel.get(dimNames.axisId); var axisModel = seriesModel.ecModel.queryComponents({ mainType: dimNames.axis, index: axisIndex, id: axisId })[0]; if (__DEV__) { if (!axisModel) { throw new Error( dimNames.axis + ' "' + retrieve( axisIndex, axisId, 0 ) + '" not found' ); } } axisIndex = axisModel.componentIndex; if (indexOf(axisIndices, axisIndex) < 0) { axisIndices.push(axisIndex); } }); } }, this); } }, /** * @private */ _autoSetOrient: function () { var dim; // Find the first axis this.eachTargetAxis(function (dimNames) { !dim && (dim = dimNames.name); }, this); this.option.orient = dim === 'y' ? 'vertical' : 'horizontal'; }, /** * @private */ _isSeriesHasAllAxesTypeOf: function (seriesModel, axisType) { // FIXME // 需要series的xAxisIndex和yAxisIndex都首先自动设置上。 // 例如series.type === scatter时。 var is = true; eachAxisDim(function (dimNames) { var seriesAxisIndex = seriesModel.get(dimNames.axisIndex); var axisModel = this.dependentModels[dimNames.axis][seriesAxisIndex]; if (!axisModel || axisModel.get('type') !== axisType) { is = false; } }, this); return is; }, /** * @private */ _setDefaultThrottle: function (inputRawOption) { // When first time user set throttle, auto throttle ends. if (inputRawOption.hasOwnProperty('throttle')) { this._autoThrottle = false; } if (this._autoThrottle) { var globalOption = this.ecModel.option; this.option.throttle = ( globalOption.animation && globalOption.animationDurationUpdate > 0 ) ? 100 : 20; } }, /** * @public */ getFirstTargetAxisModel: function () { var firstAxisModel; eachAxisDim(function (dimNames) { if (firstAxisModel == null) { var indices = this.get(dimNames.axisIndex); if (indices.length) { firstAxisModel = this.dependentModels[dimNames.axis][indices[0]]; } } }, this); return firstAxisModel; }, /** * @public * @param {Function} callback param: axisModel, dimNames, axisIndex, dataZoomModel, ecModel */ eachTargetAxis: function (callback, context) { var ecModel = this.ecModel; eachAxisDim(function (dimNames) { each$12( this.get(dimNames.axisIndex), function (axisIndex) { callback.call(context, dimNames, axisIndex, this, ecModel); }, this ); }, this); }, /** * @param {string} dimName * @param {number} axisIndex * @return {module:echarts/component/dataZoom/AxisProxy} If not found, return null/undefined. */ getAxisProxy: function (dimName, axisIndex) { return this._axisProxies[dimName + '_' + axisIndex]; }, /** * @param {string} dimName * @param {number} axisIndex * @return {module:echarts/model/Model} If not found, return null/undefined. */ getAxisModel: function (dimName, axisIndex) { var axisProxy = this.getAxisProxy(dimName, axisIndex); return axisProxy && axisProxy.getAxisModel(); }, /** * If not specified, set to undefined. * * @public * @param {Object} opt * @param {number} [opt.start] * @param {number} [opt.end] * @param {number} [opt.startValue] * @param {number} [opt.endValue] */ setRawRange: function (opt) { var thisOption = this.option; var settledOption = this.settledOption; each$12([['start', 'startValue'], ['end', 'endValue']], function (names) { // Consider the pair : // If one has value and the other one is `null/undefined`, we both set them // to `settledOption`. This strategy enables the feature to clear the original // value in `settledOption` to `null/undefined`. // But if both of them are `null/undefined`, we do not set them to `settledOption` // and keep `settledOption` with the original value. This strategy enables users to // only set but not set when calling // `dispatchAction`. // The pair is treated in the same way. if (opt[names[0]] != null || opt[names[1]] != null) { thisOption[names[0]] = settledOption[names[0]] = opt[names[0]]; thisOption[names[1]] = settledOption[names[1]] = opt[names[1]]; } }, this); updateRangeUse(this, opt); }, /** * @public * @param {Object} opt * @param {number} [opt.start] * @param {number} [opt.end] * @param {number} [opt.startValue] * @param {number} [opt.endValue] */ setCalculatedRange: function (opt) { var option = this.option; each$12(['start', 'startValue', 'end', 'endValue'], function (name) { option[name] = opt[name]; }); }, /** * @public * @return {Array.} [startPercent, endPercent] */ getPercentRange: function () { var axisProxy = this.findRepresentativeAxisProxy(); if (axisProxy) { return axisProxy.getDataPercentWindow(); } }, /** * @public * For example, chart.getModel().getComponent('dataZoom').getValueRange('y', 0); * * @param {string} [axisDimName] * @param {number} [axisIndex] * @return {Array.} [startValue, endValue] value can only be '-' or finite number. */ getValueRange: function (axisDimName, axisIndex) { if (axisDimName == null && axisIndex == null) { var axisProxy = this.findRepresentativeAxisProxy(); if (axisProxy) { return axisProxy.getDataValueWindow(); } } else { return this.getAxisProxy(axisDimName, axisIndex).getDataValueWindow(); } }, /** * @public * @param {module:echarts/model/Model} [axisModel] If axisModel given, find axisProxy * corresponding to the axisModel * @return {module:echarts/component/dataZoom/AxisProxy} */ findRepresentativeAxisProxy: function (axisModel) { if (axisModel) { return axisModel.__dzAxisProxy; } // Find the first hosted axisProxy var axisProxies = this._axisProxies; for (var key in axisProxies) { if (axisProxies.hasOwnProperty(key) && axisProxies[key].hostedBy(this)) { return axisProxies[key]; } } // If no hosted axis find not hosted axisProxy. // Consider this case: dataZoomModel1 and dataZoomModel2 control the same axis, // and the option.start or option.end settings are different. The percentRange // should follow axisProxy. // (We encounter this problem in toolbox data zoom.) for (var key in axisProxies) { if (axisProxies.hasOwnProperty(key) && !axisProxies[key].hostedBy(this)) { return axisProxies[key]; } } }, /** * @return {Array.} */ getRangePropMode: function () { return this._rangePropMode.slice(); } }); /** * Retrieve the those raw params from option, which will be cached separately. * becasue they will be overwritten by normalized/calculated values in the main * process. */ function retrieveRawOption(option) { var ret = {}; each$12( ['start', 'end', 'startValue', 'endValue', 'throttle'], function (name) { option.hasOwnProperty(name) && (ret[name] = option[name]); } ); return ret; } function updateRangeUse(dataZoomModel, inputRawOption) { var rangePropMode = dataZoomModel._rangePropMode; var rangeModeInOption = dataZoomModel.get('rangeMode'); each$12([['start', 'startValue'], ['end', 'endValue']], function (names, index) { var percentSpecified = inputRawOption[names[0]] != null; var valueSpecified = inputRawOption[names[1]] != null; if (percentSpecified && !valueSpecified) { rangePropMode[index] = 'percent'; } else if (!percentSpecified && valueSpecified) { rangePropMode[index] = 'value'; } else if (rangeModeInOption) { rangePropMode[index] = rangeModeInOption[index]; } else if (percentSpecified) { // percentSpecified && valueSpecified rangePropMode[index] = 'percent'; } // else remain its original setting. }); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var DataZoomView = Component$1.extend({ type: 'dataZoom', render: function (dataZoomModel, ecModel, api, payload) { this.dataZoomModel = dataZoomModel; this.ecModel = ecModel; this.api = api; }, /** * Find the first target coordinate system. * * @protected * @return {Object} { * grid: [ * {model: coord0, axisModels: [axis1, axis3], coordIndex: 1}, * {model: coord1, axisModels: [axis0, axis2], coordIndex: 0}, * ... * ], // cartesians must not be null/undefined. * polar: [ * {model: coord0, axisModels: [axis4], coordIndex: 0}, * ... * ], // polars must not be null/undefined. * singleAxis: [ * {model: coord0, axisModels: [], coordIndex: 0} * ] */ getTargetCoordInfo: function () { var dataZoomModel = this.dataZoomModel; var ecModel = this.ecModel; var coordSysLists = {}; dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) { var axisModel = ecModel.getComponent(dimNames.axis, axisIndex); if (axisModel) { var coordModel = axisModel.getCoordSysModel(); coordModel && save( coordModel, axisModel, coordSysLists[coordModel.mainType] || (coordSysLists[coordModel.mainType] = []), coordModel.componentIndex ); } }, this); function save(coordModel, axisModel, store, coordIndex) { var item; for (var i = 0; i < store.length; i++) { if (store[i].model === coordModel) { item = store[i]; break; } } if (!item) { store.push(item = { model: coordModel, axisModels: [], coordIndex: coordIndex }); } item.axisModels.push(axisModel); } return coordSysLists; } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var SliderZoomModel = DataZoomModel.extend({ type: 'dataZoom.slider', layoutMode: 'box', /** * @protected */ defaultOption: { show: true, // ph => placeholder. Using placehoder here because // deault value can only be drived in view stage. right: 'ph', // Default align to grid rect. top: 'ph', // Default align to grid rect. width: 'ph', // Default align to grid rect. height: 'ph', // Default align to grid rect. left: null, // Default align to grid rect. bottom: null, // Default align to grid rect. backgroundColor: 'rgba(47,69,84,0)', // Background of slider zoom component. // dataBackgroundColor: '#ddd', // Background coor of data shadow and border of box, // highest priority, remain for compatibility of // previous version, but not recommended any more. dataBackground: { lineStyle: { color: '#2f4554', width: 0.5, opacity: 0.3 }, areaStyle: { color: 'rgba(47,69,84,0.3)', opacity: 0.3 } }, borderColor: '#ddd', // border color of the box. For compatibility, // if dataBackgroundColor is set, borderColor // is ignored. fillerColor: 'rgba(167,183,204,0.4)', // Color of selected area. // handleColor: 'rgba(89,170,216,0.95)', // Color of handle. // handleIcon: 'path://M4.9,17.8c0-1.4,4.5-10.5,5.5-12.4c0-0.1,0.6-1.1,0.9-1.1c0.4,0,0.9,1,0.9,1.1c1.1,2.2,5.4,11,5.4,12.4v17.8c0,1.5-0.6,2.1-1.3,2.1H6.1c-0.7,0-1.3-0.6-1.3-2.1V17.8z', /* eslint-disable */ handleIcon: 'M8.2,13.6V3.9H6.3v9.7H3.1v14.9h3.3v9.7h1.8v-9.7h3.3V13.6H8.2z M9.7,24.4H4.8v-1.4h4.9V24.4z M9.7,19.1H4.8v-1.4h4.9V19.1z', /* eslint-enable */ // Percent of the slider height handleSize: '100%', handleStyle: { color: '#a7b7cc' }, labelPrecision: null, labelFormatter: null, showDetail: true, showDataShadow: 'auto', // Default auto decision. realtime: true, zoomLock: false, // Whether disable zoom. textStyle: { color: '#333' } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var Rect$1 = Rect; var linearMap$1 = linearMap; var asc$2 = asc; var bind$3 = bind; var each$14 = each$1; // Constants var DEFAULT_LOCATION_EDGE_GAP = 7; var DEFAULT_FRAME_BORDER_WIDTH = 1; var DEFAULT_FILLER_SIZE = 30; var HORIZONTAL = 'horizontal'; var VERTICAL = 'vertical'; var LABEL_GAP = 5; var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter']; var SliderZoomView = DataZoomView.extend({ type: 'dataZoom.slider', init: function (ecModel, api) { /** * @private * @type {Object} */ this._displayables = {}; /** * @private * @type {string} */ this._orient; /** * [0, 100] * @private */ this._range; /** * [coord of the first handle, coord of the second handle] * @private */ this._handleEnds; /** * [length, thick] * @private * @type {Array.} */ this._size; /** * @private * @type {number} */ this._handleWidth; /** * @private * @type {number} */ this._handleHeight; /** * @private */ this._location; /** * @private */ this._dragging; /** * @private */ this._dataShadowInfo; this.api = api; }, /** * @override */ render: function (dataZoomModel, ecModel, api, payload) { SliderZoomView.superApply(this, 'render', arguments); createOrUpdate( this, '_dispatchZoomAction', this.dataZoomModel.get('throttle'), 'fixRate' ); this._orient = dataZoomModel.get('orient'); if (this.dataZoomModel.get('show') === false) { this.group.removeAll(); return; } // Notice: this._resetInterval() should not be executed when payload.type // is 'dataZoom', origin this._range should be maintained, otherwise 'pan' // or 'zoom' info will be missed because of 'throttle' of this.dispatchAction, if (!payload || payload.type !== 'dataZoom' || payload.from !== this.uid) { this._buildView(); } this._updateView(); }, /** * @override */ remove: function () { SliderZoomView.superApply(this, 'remove', arguments); clear(this, '_dispatchZoomAction'); }, /** * @override */ dispose: function () { SliderZoomView.superApply(this, 'dispose', arguments); clear(this, '_dispatchZoomAction'); }, _buildView: function () { var thisGroup = this.group; thisGroup.removeAll(); this._resetLocation(); this._resetInterval(); var barGroup = this._displayables.barGroup = new Group(); this._renderBackground(); this._renderHandle(); this._renderDataShadow(); thisGroup.add(barGroup); this._positionGroup(); }, /** * @private */ _resetLocation: function () { var dataZoomModel = this.dataZoomModel; var api = this.api; // If some of x/y/width/height are not specified, // auto-adapt according to target grid. var coordRect = this._findCoordRect(); var ecSize = {width: api.getWidth(), height: api.getHeight()}; // Default align by coordinate system rect. var positionInfo = this._orient === HORIZONTAL ? { // Why using 'right', because right should be used in vertical, // and it is better to be consistent for dealing with position param merge. right: ecSize.width - coordRect.x - coordRect.width, top: (ecSize.height - DEFAULT_FILLER_SIZE - DEFAULT_LOCATION_EDGE_GAP), width: coordRect.width, height: DEFAULT_FILLER_SIZE } : { // vertical right: DEFAULT_LOCATION_EDGE_GAP, top: coordRect.y, width: DEFAULT_FILLER_SIZE, height: coordRect.height }; // Do not write back to option and replace value 'ph', because // the 'ph' value should be recalculated when resize. var layoutParams = getLayoutParams(dataZoomModel.option); // Replace the placeholder value. each$1(['right', 'top', 'width', 'height'], function (name) { if (layoutParams[name] === 'ph') { layoutParams[name] = positionInfo[name]; } }); var layoutRect = getLayoutRect( layoutParams, ecSize, dataZoomModel.padding ); this._location = {x: layoutRect.x, y: layoutRect.y}; this._size = [layoutRect.width, layoutRect.height]; this._orient === VERTICAL && this._size.reverse(); }, /** * @private */ _positionGroup: function () { var thisGroup = this.group; var location = this._location; var orient = this._orient; // Just use the first axis to determine mapping. var targetAxisModel = this.dataZoomModel.getFirstTargetAxisModel(); var inverse = targetAxisModel && targetAxisModel.get('inverse'); var barGroup = this._displayables.barGroup; var otherAxisInverse = (this._dataShadowInfo || {}).otherAxisInverse; // Transform barGroup. barGroup.attr( (orient === HORIZONTAL && !inverse) ? {scale: otherAxisInverse ? [1, 1] : [1, -1]} : (orient === HORIZONTAL && inverse) ? {scale: otherAxisInverse ? [-1, 1] : [-1, -1]} : (orient === VERTICAL && !inverse) ? {scale: otherAxisInverse ? [1, -1] : [1, 1], rotation: Math.PI / 2} // Dont use Math.PI, considering shadow direction. : {scale: otherAxisInverse ? [-1, -1] : [-1, 1], rotation: Math.PI / 2} ); // Position barGroup var rect = thisGroup.getBoundingRect([barGroup]); thisGroup.attr('position', [location.x - rect.x, location.y - rect.y]); }, /** * @private */ _getViewExtent: function () { return [0, this._size[0]]; }, _renderBackground: function () { var dataZoomModel = this.dataZoomModel; var size = this._size; var barGroup = this._displayables.barGroup; barGroup.add(new Rect$1({ silent: true, shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: dataZoomModel.get('backgroundColor') }, z2: -40 })); // Click panel, over shadow, below handles. barGroup.add(new Rect$1({ shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: 'transparent' }, z2: 0, onclick: bind(this._onClickPanelClick, this) })); }, _renderDataShadow: function () { var info = this._dataShadowInfo = this._prepareDataShadowInfo(); if (!info) { return; } var size = this._size; var seriesModel = info.series; var data = seriesModel.getRawData(); var otherDim = seriesModel.getShadowDim ? seriesModel.getShadowDim() // @see candlestick : info.otherDim; if (otherDim == null) { return; } var otherDataExtent = data.getDataExtent(otherDim); // Nice extent. var otherOffset = (otherDataExtent[1] - otherDataExtent[0]) * 0.3; otherDataExtent = [ otherDataExtent[0] - otherOffset, otherDataExtent[1] + otherOffset ]; var otherShadowExtent = [0, size[1]]; var thisShadowExtent = [0, size[0]]; var areaPoints = [[size[0], 0], [0, 0]]; var linePoints = []; var step = thisShadowExtent[1] / (data.count() - 1); var thisCoord = 0; // Optimize for large data shadow var stride = Math.round(data.count() / size[0]); var lastIsEmpty; data.each([otherDim], function (value, index) { if (stride > 0 && (index % stride)) { thisCoord += step; return; } // FIXME // Should consider axis.min/axis.max when drawing dataShadow. // FIXME // 应该使用统一的空判断?还是在list里进行空判断? var isEmpty = value == null || isNaN(value) || value === ''; // See #4235. var otherCoord = isEmpty ? 0 : linearMap$1(value, otherDataExtent, otherShadowExtent, true); // Attempt to draw data shadow precisely when there are empty value. if (isEmpty && !lastIsEmpty && index) { areaPoints.push([areaPoints[areaPoints.length - 1][0], 0]); linePoints.push([linePoints[linePoints.length - 1][0], 0]); } else if (!isEmpty && lastIsEmpty) { areaPoints.push([thisCoord, 0]); linePoints.push([thisCoord, 0]); } areaPoints.push([thisCoord, otherCoord]); linePoints.push([thisCoord, otherCoord]); thisCoord += step; lastIsEmpty = isEmpty; }); var dataZoomModel = this.dataZoomModel; // var dataBackgroundModel = dataZoomModel.getModel('dataBackground'); this._displayables.barGroup.add(new Polygon({ shape: {points: areaPoints}, style: defaults( {fill: dataZoomModel.get('dataBackgroundColor')}, dataZoomModel.getModel('dataBackground.areaStyle').getAreaStyle() ), silent: true, z2: -20 })); this._displayables.barGroup.add(new Polyline({ shape: {points: linePoints}, style: dataZoomModel.getModel('dataBackground.lineStyle').getLineStyle(), silent: true, z2: -19 })); }, _prepareDataShadowInfo: function () { var dataZoomModel = this.dataZoomModel; var showDataShadow = dataZoomModel.get('showDataShadow'); if (showDataShadow === false) { return; } // Find a representative series. var result; var ecModel = this.ecModel; dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) { var seriesModels = dataZoomModel .getAxisProxy(dimNames.name, axisIndex) .getTargetSeriesModels(); each$1(seriesModels, function (seriesModel) { if (result) { return; } if (showDataShadow !== true && indexOf( SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type') ) < 0 ) { return; } var thisAxis = ecModel.getComponent(dimNames.axis, axisIndex).axis; var otherDim = getOtherDim(dimNames.name); var otherAxisInverse; var coordSys = seriesModel.coordinateSystem; if (otherDim != null && coordSys.getOtherAxis) { otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse; } otherDim = seriesModel.getData().mapDimension(otherDim); result = { thisAxis: thisAxis, series: seriesModel, thisDim: dimNames.name, otherDim: otherDim, otherAxisInverse: otherAxisInverse }; }, this); }, this); return result; }, _renderHandle: function () { var displaybles = this._displayables; var handles = displaybles.handles = []; var handleLabels = displaybles.handleLabels = []; var barGroup = this._displayables.barGroup; var size = this._size; var dataZoomModel = this.dataZoomModel; barGroup.add(displaybles.filler = new Rect$1({ draggable: true, cursor: getCursor(this._orient), drift: bind$3(this._onDragMove, this, 'all'), ondragstart: bind$3(this._showDataInfo, this, true), ondragend: bind$3(this._onDragEnd, this), onmouseover: bind$3(this._showDataInfo, this, true), onmouseout: bind$3(this._showDataInfo, this, false), style: { fill: dataZoomModel.get('fillerColor'), textPosition: 'inside' } })); // Frame border. barGroup.add(new Rect$1({ silent: true, subPixelOptimize: true, shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { stroke: dataZoomModel.get('dataBackgroundColor') || dataZoomModel.get('borderColor'), lineWidth: DEFAULT_FRAME_BORDER_WIDTH, fill: 'rgba(0,0,0,0)' } })); each$14([0, 1], function (handleIndex) { var path = createIcon( dataZoomModel.get('handleIcon'), { cursor: getCursor(this._orient), draggable: true, drift: bind$3(this._onDragMove, this, handleIndex), ondragend: bind$3(this._onDragEnd, this), onmouseover: bind$3(this._showDataInfo, this, true), onmouseout: bind$3(this._showDataInfo, this, false) }, {x: -1, y: 0, width: 2, height: 2} ); var bRect = path.getBoundingRect(); this._handleHeight = parsePercent$1(dataZoomModel.get('handleSize'), this._size[1]); this._handleWidth = bRect.width / bRect.height * this._handleHeight; path.setStyle(dataZoomModel.getModel('handleStyle').getItemStyle()); var handleColor = dataZoomModel.get('handleColor'); // Compatitable with previous version if (handleColor != null) { path.style.fill = handleColor; } barGroup.add(handles[handleIndex] = path); var textStyleModel = dataZoomModel.textStyleModel; this.group.add( handleLabels[handleIndex] = new Text({ silent: true, invisible: true, style: { x: 0, y: 0, text: '', textVerticalAlign: 'middle', textAlign: 'center', textFill: textStyleModel.getTextColor(), textFont: textStyleModel.getFont() }, z2: 10 })); }, this); }, /** * @private */ _resetInterval: function () { var range = this._range = this.dataZoomModel.getPercentRange(); var viewExtent = this._getViewExtent(); this._handleEnds = [ linearMap$1(range[0], [0, 100], viewExtent, true), linearMap$1(range[1], [0, 100], viewExtent, true) ]; }, /** * @private * @param {(number|string)} handleIndex 0 or 1 or 'all' * @param {number} delta * @return {boolean} changed */ _updateInterval: function (handleIndex, delta) { var dataZoomModel = this.dataZoomModel; var handleEnds = this._handleEnds; var viewExtend = this._getViewExtent(); var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); var percentExtent = [0, 100]; sliderMove( delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap$1(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap$1(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null ); var lastRange = this._range; var range = this._range = asc$2([ linearMap$1(handleEnds[0], viewExtend, percentExtent, true), linearMap$1(handleEnds[1], viewExtend, percentExtent, true) ]); return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1]; }, /** * @private */ _updateView: function (nonRealtime) { var displaybles = this._displayables; var handleEnds = this._handleEnds; var handleInterval = asc$2(handleEnds.slice()); var size = this._size; each$14([0, 1], function (handleIndex) { // Handles var handle = displaybles.handles[handleIndex]; var handleHeight = this._handleHeight; handle.attr({ scale: [handleHeight / 2, handleHeight / 2], position: [handleEnds[handleIndex], size[1] / 2 - handleHeight / 2] }); }, this); // Filler displaybles.filler.setShape({ x: handleInterval[0], y: 0, width: handleInterval[1] - handleInterval[0], height: size[1] }); this._updateDataInfo(nonRealtime); }, /** * @private */ _updateDataInfo: function (nonRealtime) { var dataZoomModel = this.dataZoomModel; var displaybles = this._displayables; var handleLabels = displaybles.handleLabels; var orient = this._orient; var labelTexts = ['', '']; // FIXME // date型,支持formatter,autoformatter(ec2 date.getAutoFormatter) if (dataZoomModel.get('showDetail')) { var axisProxy = dataZoomModel.findRepresentativeAxisProxy(); if (axisProxy) { var axis = axisProxy.getAxisModel().axis; var range = this._range; var dataInterval = nonRealtime // See #4434, data and axis are not processed and reset yet in non-realtime mode. ? axisProxy.calculateDataWindow({ start: range[0], end: range[1] }).valueWindow : axisProxy.getDataValueWindow(); labelTexts = [ this._formatLabel(dataInterval[0], axis), this._formatLabel(dataInterval[1], axis) ]; } } var orderedHandleEnds = asc$2(this._handleEnds.slice()); setLabel.call(this, 0); setLabel.call(this, 1); function setLabel(handleIndex) { // Label // Text should not transform by barGroup. // Ignore handlers transform var barTransform = getTransform( displaybles.handles[handleIndex].parent, this.group ); var direction = transformDirection( handleIndex === 0 ? 'right' : 'left', barTransform ); var offset = this._handleWidth / 2 + LABEL_GAP; var textPoint = applyTransform$1( [ orderedHandleEnds[handleIndex] + (handleIndex === 0 ? -offset : offset), this._size[1] / 2 ], barTransform ); handleLabels[handleIndex].setStyle({ x: textPoint[0], y: textPoint[1], textVerticalAlign: orient === HORIZONTAL ? 'middle' : direction, textAlign: orient === HORIZONTAL ? direction : 'center', text: labelTexts[handleIndex] }); } }, /** * @private */ _formatLabel: function (value, axis) { var dataZoomModel = this.dataZoomModel; var labelFormatter = dataZoomModel.get('labelFormatter'); var labelPrecision = dataZoomModel.get('labelPrecision'); if (labelPrecision == null || labelPrecision === 'auto') { labelPrecision = axis.getPixelPrecision(); } var valueStr = (value == null || isNaN(value)) ? '' // FIXME Glue code : (axis.type === 'category' || axis.type === 'time') ? axis.scale.getLabel(Math.round(value)) // param of toFixed should less then 20. : value.toFixed(Math.min(labelPrecision, 20)); return isFunction$1(labelFormatter) ? labelFormatter(value, valueStr) : isString(labelFormatter) ? labelFormatter.replace('{value}', valueStr) : valueStr; }, /** * @private * @param {boolean} showOrHide true: show, false: hide */ _showDataInfo: function (showOrHide) { // Always show when drgging. showOrHide = this._dragging || showOrHide; var handleLabels = this._displayables.handleLabels; handleLabels[0].attr('invisible', !showOrHide); handleLabels[1].attr('invisible', !showOrHide); }, _onDragMove: function (handleIndex, dx, dy, event) { this._dragging = true; // For mobile device, prevent screen slider on the button. stop(event.event); // Transform dx, dy to bar coordination. var barTransform = this._displayables.barGroup.getLocalTransform(); var vertex = applyTransform$1([dx, dy], barTransform, true); var changed = this._updateInterval(handleIndex, vertex[0]); var realtime = this.dataZoomModel.get('realtime'); this._updateView(!realtime); // Avoid dispatch dataZoom repeatly but range not changed, // which cause bad visual effect when progressive enabled. changed && realtime && this._dispatchZoomAction(); }, _onDragEnd: function () { this._dragging = false; this._showDataInfo(false); // While in realtime mode and stream mode, dispatch action when // drag end will cause the whole view rerender, which is unnecessary. var realtime = this.dataZoomModel.get('realtime'); !realtime && this._dispatchZoomAction(); }, _onClickPanelClick: function (e) { var size = this._size; var localPoint = this._displayables.barGroup.transformCoordToLocal(e.offsetX, e.offsetY); if (localPoint[0] < 0 || localPoint[0] > size[0] || localPoint[1] < 0 || localPoint[1] > size[1] ) { return; } var handleEnds = this._handleEnds; var center = (handleEnds[0] + handleEnds[1]) / 2; var changed = this._updateInterval('all', localPoint[0] - center); this._updateView(); changed && this._dispatchZoomAction(); }, /** * This action will be throttled. * @private */ _dispatchZoomAction: function () { var range = this._range; this.api.dispatchAction({ type: 'dataZoom', from: this.uid, dataZoomId: this.dataZoomModel.id, start: range[0], end: range[1] }); }, /** * @private */ _findCoordRect: function () { // Find the grid coresponding to the first axis referred by dataZoom. var rect; each$14(this.getTargetCoordInfo(), function (coordInfoList) { if (!rect && coordInfoList.length) { var coordSys = coordInfoList[0].model.coordinateSystem; rect = coordSys.getRect && coordSys.getRect(); } }); if (!rect) { var width = this.api.getWidth(); var height = this.api.getHeight(); rect = { x: width * 0.2, y: height * 0.2, width: width * 0.6, height: height * 0.6 }; } return rect; } }); function getOtherDim(thisDim) { // FIXME // 这个逻辑和getOtherAxis里一致,但是写在这里是否不好 var map$$1 = {x: 'y', y: 'x', radius: 'angle', angle: 'radius'}; return map$$1[thisDim]; } function getCursor(orient) { return orient === 'vertical' ? 'ns-resize' : 'ew-resize'; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ registerProcessor({ // `dataZoomProcessor` will only be performed in needed series. Consider if // there is a line series and a pie series, it is better not to update the // line series if only pie series is needed to be updated. getTargetSeries: function (ecModel) { var seriesModelMap = createHashMap(); ecModel.eachComponent('dataZoom', function (dataZoomModel) { dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { var axisProxy = dataZoomModel.getAxisProxy(dimNames.name, axisIndex); each$1(axisProxy.getTargetSeriesModels(), function (seriesModel) { seriesModelMap.set(seriesModel.uid, seriesModel); }); }); }); return seriesModelMap; }, modifyOutputEnd: true, // Consider appendData, where filter should be performed. Because data process is // in block mode currently, it is not need to worry about that the overallProgress // execute every frame. overallReset: function (ecModel, api) { ecModel.eachComponent('dataZoom', function (dataZoomModel) { // We calculate window and reset axis here but not in model // init stage and not after action dispatch handler, because // reset should be called after seriesData.restoreData. dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { dataZoomModel.getAxisProxy(dimNames.name, axisIndex).reset(dataZoomModel, api); }); // Caution: data zoom filtering is order sensitive when using // percent range and no min/max/scale set on axis. // For example, we have dataZoom definition: // [ // {xAxisIndex: 0, start: 30, end: 70}, // {yAxisIndex: 0, start: 20, end: 80} // ] // In this case, [20, 80] of y-dataZoom should be based on data // that have filtered by x-dataZoom using range of [30, 70], // but should not be based on full raw data. Thus sliding // x-dataZoom will change both ranges of xAxis and yAxis, // while sliding y-dataZoom will only change the range of yAxis. // So we should filter x-axis after reset x-axis immediately, // and then reset y-axis and filter y-axis. dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { dataZoomModel.getAxisProxy(dimNames.name, axisIndex).filterData(dataZoomModel, api); }); }); ecModel.eachComponent('dataZoom', function (dataZoomModel) { // Fullfill all of the range props so that user // is able to get them from chart.getOption(). var axisProxy = dataZoomModel.findRepresentativeAxisProxy(); var percentRange = axisProxy.getDataPercentWindow(); var valueRange = axisProxy.getDataValueWindow(); dataZoomModel.setCalculatedRange({ start: percentRange[0], end: percentRange[1], startValue: valueRange[0], endValue: valueRange[1] }); }); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ registerAction('dataZoom', function (payload, ecModel) { var linkedNodesFinder = createLinkedNodesFinder( bind(ecModel.eachComponent, ecModel, 'dataZoom'), eachAxisDim$1, function (model, dimNames) { return model.get(dimNames.axisIndex); } ); var effectedModels = []; ecModel.eachComponent( {mainType: 'dataZoom', query: payload}, function (model, index) { effectedModels.push.apply( effectedModels, linkedNodesFinder(model).nodes ); } ); each$1(effectedModels, function (dataZoomModel, index) { dataZoomModel.setRawRange({ start: payload.start, end: payload.end, startValue: payload.startValue, endValue: payload.endValue }); }); }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ DataZoomModel.extend({ type: 'dataZoom.inside', /** * @protected */ defaultOption: { disabled: false, // Whether disable this inside zoom. zoomLock: false, // Whether disable zoom but only pan. zoomOnMouseWheel: true, // Can be: true / false / 'shift' / 'ctrl' / 'alt'. moveOnMouseMove: true, // Can be: true / false / 'shift' / 'ctrl' / 'alt'. moveOnMouseWheel: false, // Can be: true / false / 'shift' / 'ctrl' / 'alt'. preventDefaultMouseMove: true } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var ATTR$1 = '\0_ec_interaction_mutex'; function take(zr, resourceKey, userKey) { var store = getStore(zr); store[resourceKey] = userKey; } function release(zr, resourceKey, userKey) { var store = getStore(zr); var uKey = store[resourceKey]; if (uKey === userKey) { store[resourceKey] = null; } } function isTaken(zr, resourceKey) { return !!getStore(zr)[resourceKey]; } function getStore(zr) { return zr[ATTR$1] || (zr[ATTR$1] = {}); } /** * payload: { * type: 'takeGlobalCursor', * key: 'dataZoomSelect', or 'brush', or ..., * If no userKey, release global cursor. * } */ registerAction( {type: 'takeGlobalCursor', event: 'globalCursorTaken', update: 'update'}, function () {} ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * @alias module:echarts/component/helper/RoamController * @constructor * @mixin {module:zrender/mixin/Eventful} * * @param {module:zrender/zrender~ZRender} zr */ function RoamController(zr) { /** * @type {Function} */ this.pointerChecker; /** * @type {module:zrender} */ this._zr = zr; /** * @type {Object} */ this._opt = {}; // Avoid two roamController bind the same handler var bind$$1 = bind; var mousedownHandler = bind$$1(mousedown, this); var mousemoveHandler = bind$$1(mousemove, this); var mouseupHandler = bind$$1(mouseup, this); var mousewheelHandler = bind$$1(mousewheel, this); var pinchHandler = bind$$1(pinch, this); Eventful.call(this); /** * @param {Function} pointerChecker * input: x, y * output: boolean */ this.setPointerChecker = function (pointerChecker) { this.pointerChecker = pointerChecker; }; /** * Notice: only enable needed types. For example, if 'zoom' * is not needed, 'zoom' should not be enabled, otherwise * default mousewheel behaviour (scroll page) will be disabled. * * @param {boolean|string} [controlType=true] Specify the control type, * which can be null/undefined or true/false * or 'pan/move' or 'zoom'/'scale' * @param {Object} [opt] * @param {Object} [opt.zoomOnMouseWheel=true] The value can be: true / false / 'shift' / 'ctrl' / 'alt'. * @param {Object} [opt.moveOnMouseMove=true] The value can be: true / false / 'shift' / 'ctrl' / 'alt'. * @param {Object} [opt.moveOnMouseWheel=false] The value can be: true / false / 'shift' / 'ctrl' / 'alt'. * @param {Object} [opt.preventDefaultMouseMove=true] When pan. */ this.enable = function (controlType, opt) { // Disable previous first this.disable(); this._opt = defaults(clone(opt) || {}, { zoomOnMouseWheel: true, moveOnMouseMove: true, // By default, wheel do not trigger move. moveOnMouseWheel: false, preventDefaultMouseMove: true }); if (controlType == null) { controlType = true; } if (controlType === true || (controlType === 'move' || controlType === 'pan')) { zr.on('mousedown', mousedownHandler); zr.on('mousemove', mousemoveHandler); zr.on('mouseup', mouseupHandler); } if (controlType === true || (controlType === 'scale' || controlType === 'zoom')) { zr.on('mousewheel', mousewheelHandler); zr.on('pinch', pinchHandler); } }; this.disable = function () { zr.off('mousedown', mousedownHandler); zr.off('mousemove', mousemoveHandler); zr.off('mouseup', mouseupHandler); zr.off('mousewheel', mousewheelHandler); zr.off('pinch', pinchHandler); }; this.dispose = this.disable; this.isDragging = function () { return this._dragging; }; this.isPinching = function () { return this._pinching; }; } mixin(RoamController, Eventful); function mousedown(e) { if (isMiddleOrRightButtonOnMouseUpDown(e) || (e.target && e.target.draggable) ) { return; } var x = e.offsetX; var y = e.offsetY; // Only check on mosedown, but not mousemove. // Mouse can be out of target when mouse moving. if (this.pointerChecker && this.pointerChecker(e, x, y)) { this._x = x; this._y = y; this._dragging = true; } } function mousemove(e) { if (!this._dragging || !isAvailableBehavior('moveOnMouseMove', e, this._opt) || e.gestureEvent === 'pinch' || isTaken(this._zr, 'globalPan') ) { return; } var x = e.offsetX; var y = e.offsetY; var oldX = this._x; var oldY = this._y; var dx = x - oldX; var dy = y - oldY; this._x = x; this._y = y; this._opt.preventDefaultMouseMove && stop(e.event); trigger(this, 'pan', 'moveOnMouseMove', e, { dx: dx, dy: dy, oldX: oldX, oldY: oldY, newX: x, newY: y }); } function mouseup(e) { if (!isMiddleOrRightButtonOnMouseUpDown(e)) { this._dragging = false; } } function mousewheel(e) { var shouldZoom = isAvailableBehavior('zoomOnMouseWheel', e, this._opt); var shouldMove = isAvailableBehavior('moveOnMouseWheel', e, this._opt); var wheelDelta = e.wheelDelta; var absWheelDeltaDelta = Math.abs(wheelDelta); var originX = e.offsetX; var originY = e.offsetY; // wheelDelta maybe -0 in chrome mac. if (wheelDelta === 0 || (!shouldZoom && !shouldMove)) { return; } // If both `shouldZoom` and `shouldMove` is true, trigger // their event both, and the final behavior is determined // by event listener themselves. if (shouldZoom) { // Convenience: // Mac and VM Windows on Mac: scroll up: zoom out. // Windows: scroll up: zoom in. // FIXME: Should do more test in different environment. // wheelDelta is too complicated in difference nvironment // (https://developer.mozilla.org/en-US/docs/Web/Events/mousewheel), // although it has been normallized by zrender. // wheelDelta of mouse wheel is bigger than touch pad. var factor = absWheelDeltaDelta > 3 ? 1.4 : absWheelDeltaDelta > 1 ? 1.2 : 1.1; var scale = wheelDelta > 0 ? factor : 1 / factor; checkPointerAndTrigger(this, 'zoom', 'zoomOnMouseWheel', e, { scale: scale, originX: originX, originY: originY }); } if (shouldMove) { // FIXME: Should do more test in different environment. var absDelta = Math.abs(wheelDelta); // wheelDelta of mouse wheel is bigger than touch pad. var scrollDelta = (wheelDelta > 0 ? 1 : -1) * (absDelta > 3 ? 0.4 : absDelta > 1 ? 0.15 : 0.05); checkPointerAndTrigger(this, 'scrollMove', 'moveOnMouseWheel', e, { scrollDelta: scrollDelta, originX: originX, originY: originY }); } } function pinch(e) { if (isTaken(this._zr, 'globalPan')) { return; } var scale = e.pinchScale > 1 ? 1.1 : 1 / 1.1; checkPointerAndTrigger(this, 'zoom', null, e, { scale: scale, originX: e.pinchX, originY: e.pinchY }); } function checkPointerAndTrigger(controller, eventName, behaviorToCheck, e, contollerEvent) { if (controller.pointerChecker && controller.pointerChecker(e, contollerEvent.originX, contollerEvent.originY) ) { // When mouse is out of roamController rect, // default befavoius should not be be disabled, otherwise // page sliding is disabled, contrary to expectation. stop(e.event); trigger(controller, eventName, behaviorToCheck, e, contollerEvent); } } function trigger(controller, eventName, behaviorToCheck, e, contollerEvent) { // Also provide behavior checker for event listener, for some case that // multiple components share one listener. contollerEvent.isAvailableBehavior = bind(isAvailableBehavior, null, behaviorToCheck, e); controller.trigger(eventName, contollerEvent); } // settings: { // zoomOnMouseWheel // moveOnMouseMove // moveOnMouseWheel // } // The value can be: true / false / 'shift' / 'ctrl' / 'alt'. function isAvailableBehavior(behaviorToCheck, e, settings) { var setting = settings[behaviorToCheck]; return !behaviorToCheck || ( setting && (!isString(setting) || e.event[setting + 'Key']) ); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Only create one roam controller for each coordinate system. // one roam controller might be refered by two inside data zoom // components (for example, one for x and one for y). When user // pan or zoom, only dispatch one action for those data zoom // components. var ATTR = '\0_ec_dataZoom_roams'; /** * @public * @param {module:echarts/ExtensionAPI} api * @param {Object} dataZoomInfo * @param {string} dataZoomInfo.coordId * @param {Function} dataZoomInfo.containsPoint * @param {Array.} dataZoomInfo.allCoordIds * @param {string} dataZoomInfo.dataZoomId * @param {Object} dataZoomInfo.getRange * @param {Function} dataZoomInfo.getRange.pan * @param {Function} dataZoomInfo.getRange.zoom * @param {Function} dataZoomInfo.getRange.scrollMove * @param {boolean} dataZoomInfo.dataZoomModel */ function register$1(api, dataZoomInfo) { var store = giveStore(api); var theDataZoomId = dataZoomInfo.dataZoomId; var theCoordId = dataZoomInfo.coordId; // Do clean when a dataZoom changes its target coordnate system. // Avoid memory leak, dispose all not-used-registered. each$1(store, function (record, coordId) { var dataZoomInfos = record.dataZoomInfos; if (dataZoomInfos[theDataZoomId] && indexOf(dataZoomInfo.allCoordIds, theCoordId) < 0 ) { delete dataZoomInfos[theDataZoomId]; record.count--; } }); cleanStore(store); var record = store[theCoordId]; // Create if needed. if (!record) { record = store[theCoordId] = { coordId: theCoordId, dataZoomInfos: {}, count: 0 }; record.controller = createController(api, record); record.dispatchAction = curry(dispatchAction, api); } // Update reference of dataZoom. !(record.dataZoomInfos[theDataZoomId]) && record.count++; record.dataZoomInfos[theDataZoomId] = dataZoomInfo; var controllerParams = mergeControllerParams(record.dataZoomInfos); record.controller.enable(controllerParams.controlType, controllerParams.opt); // Consider resize, area should be always updated. record.controller.setPointerChecker(dataZoomInfo.containsPoint); // Update throttle. createOrUpdate( record, 'dispatchAction', dataZoomInfo.dataZoomModel.get('throttle', true), 'fixRate' ); } /** * @public * @param {module:echarts/ExtensionAPI} api * @param {string} dataZoomId */ function unregister$1(api, dataZoomId) { var store = giveStore(api); each$1(store, function (record) { record.controller.dispose(); var dataZoomInfos = record.dataZoomInfos; if (dataZoomInfos[dataZoomId]) { delete dataZoomInfos[dataZoomId]; record.count--; } }); cleanStore(store); } /** * @public */ function generateCoordId(coordModel) { return coordModel.type + '\0_' + coordModel.id; } /** * Key: coordId, value: {dataZoomInfos: [], count, controller} * @type {Array.} */ function giveStore(api) { // Mount store on zrender instance, so that we do not // need to worry about dispose. var zr = api.getZr(); return zr[ATTR] || (zr[ATTR] = {}); } function createController(api, newRecord) { var controller = new RoamController(api.getZr()); each$1(['pan', 'zoom', 'scrollMove'], function (eventName) { controller.on(eventName, function (event) { var batch = []; each$1(newRecord.dataZoomInfos, function (info) { // Check whether the behaviors (zoomOnMouseWheel, moveOnMouseMove, // moveOnMouseWheel, ...) enabled. if (!event.isAvailableBehavior(info.dataZoomModel.option)) { return; } var method = (info.getRange || {})[eventName]; var range = method && method(newRecord.controller, event); !info.dataZoomModel.get('disabled', true) && range && batch.push({ dataZoomId: info.dataZoomId, start: range[0], end: range[1] }); }); batch.length && newRecord.dispatchAction(batch); }); }); return controller; } function cleanStore(store) { each$1(store, function (record, coordId) { if (!record.count) { record.controller.dispose(); delete store[coordId]; } }); } /** * This action will be throttled. */ function dispatchAction(api, batch) { api.dispatchAction({ type: 'dataZoom', batch: batch }); } /** * Merge roamController settings when multiple dataZooms share one roamController. */ function mergeControllerParams(dataZoomInfos) { var controlType; // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated // as string, it is probably revert to reserved word by compress tool. See #7411. var prefix = 'type_'; var typePriority = { 'type_true': 2, 'type_move': 1, 'type_false': 0, 'type_undefined': -1 }; var preventDefaultMouseMove = true; each$1(dataZoomInfos, function (dataZoomInfo) { var dataZoomModel = dataZoomInfo.dataZoomModel; var oneType = dataZoomModel.get('disabled', true) ? false : dataZoomModel.get('zoomLock', true) ? 'move' : true; if (typePriority[prefix + oneType] > typePriority[prefix + controlType]) { controlType = oneType; } // Prevent default move event by default. If one false, do not prevent. Otherwise // users may be confused why it does not work when multiple insideZooms exist. preventDefaultMouseMove &= dataZoomModel.get('preventDefaultMouseMove', true); }); return { controlType: controlType, opt: { // RoamController will enable all of these functionalities, // and the final behavior is determined by its event listener // provided by each inside zoom. zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true, preventDefaultMouseMove: !!preventDefaultMouseMove } }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var bind$4 = bind; var InsideZoomView = DataZoomView.extend({ type: 'dataZoom.inside', /** * @override */ init: function (ecModel, api) { /** * 'throttle' is used in this.dispatchAction, so we save range * to avoid missing some 'pan' info. * @private * @type {Array.} */ this._range; }, /** * @override */ render: function (dataZoomModel, ecModel, api, payload) { InsideZoomView.superApply(this, 'render', arguments); // Hence the `throttle` util ensures to preserve command order, // here simply updating range all the time will not cause missing // any of the the roam change. this._range = dataZoomModel.getPercentRange(); // Reset controllers. each$1(this.getTargetCoordInfo(), function (coordInfoList, coordSysName) { var allCoordIds = map(coordInfoList, function (coordInfo) { return generateCoordId(coordInfo.model); }); each$1(coordInfoList, function (coordInfo) { var coordModel = coordInfo.model; var getRange = {}; each$1(['pan', 'zoom', 'scrollMove'], function (eventName) { getRange[eventName] = bind$4(roamHandlers[eventName], this, coordInfo, coordSysName); }, this); register$1( api, { coordId: generateCoordId(coordModel), allCoordIds: allCoordIds, containsPoint: function (e, x, y) { return coordModel.coordinateSystem.containPoint([x, y]); }, dataZoomId: dataZoomModel.id, dataZoomModel: dataZoomModel, getRange: getRange } ); }, this); }, this); }, /** * @override */ dispose: function () { unregister$1(this.api, this.dataZoomModel.id); InsideZoomView.superApply(this, 'dispose', arguments); this._range = null; } }); var roamHandlers = { /** * @this {module:echarts/component/dataZoom/InsideZoomView} */ zoom: function (coordInfo, coordSysName, controller, e) { var lastRange = this._range; var range = lastRange.slice(); // Calculate transform by the first axis. var axisModel = coordInfo.axisModels[0]; if (!axisModel) { return; } var directionInfo = getDirectionInfo[coordSysName]( null, [e.originX, e.originY], axisModel, controller, coordInfo ); var percentPoint = ( directionInfo.signal > 0 ? (directionInfo.pixelStart + directionInfo.pixelLength - directionInfo.pixel) : (directionInfo.pixel - directionInfo.pixelStart) ) / directionInfo.pixelLength * (range[1] - range[0]) + range[0]; var scale = Math.max(1 / e.scale, 0); range[0] = (range[0] - percentPoint) * scale + percentPoint; range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range. var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan); this._range = range; if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) { return range; } }, /** * @this {module:echarts/component/dataZoom/InsideZoomView} */ pan: makeMover(function (range, axisModel, coordInfo, coordSysName, controller, e) { var directionInfo = getDirectionInfo[coordSysName]( [e.oldX, e.oldY], [e.newX, e.newY], axisModel, controller, coordInfo ); return directionInfo.signal * (range[1] - range[0]) * directionInfo.pixel / directionInfo.pixelLength; }), /** * @this {module:echarts/component/dataZoom/InsideZoomView} */ scrollMove: makeMover(function (range, axisModel, coordInfo, coordSysName, controller, e) { var directionInfo = getDirectionInfo[coordSysName]( [0, 0], [e.scrollDelta, e.scrollDelta], axisModel, controller, coordInfo ); return directionInfo.signal * (range[1] - range[0]) * e.scrollDelta; }) }; function makeMover(getPercentDelta) { return function (coordInfo, coordSysName, controller, e) { var lastRange = this._range; var range = lastRange.slice(); // Calculate transform by the first axis. var axisModel = coordInfo.axisModels[0]; if (!axisModel) { return; } var percentDelta = getPercentDelta( range, axisModel, coordInfo, coordSysName, controller, e ); sliderMove(percentDelta, range, [0, 100], 'all'); this._range = range; if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) { return range; } }; } var getDirectionInfo = { grid: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var ret = {}; var rect = coordInfo.model.coordinateSystem.getRect(); oldPoint = oldPoint || [0, 0]; if (axis.dim === 'x') { ret.pixel = newPoint[0] - oldPoint[0]; ret.pixelLength = rect.width; ret.pixelStart = rect.x; ret.signal = axis.inverse ? 1 : -1; } else { // axis.dim === 'y' ret.pixel = newPoint[1] - oldPoint[1]; ret.pixelLength = rect.height; ret.pixelStart = rect.y; ret.signal = axis.inverse ? -1 : 1; } return ret; }, polar: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var ret = {}; var polar = coordInfo.model.coordinateSystem; var radiusExtent = polar.getRadiusAxis().getExtent(); var angleExtent = polar.getAngleAxis().getExtent(); oldPoint = oldPoint ? polar.pointToCoord(oldPoint) : [0, 0]; newPoint = polar.pointToCoord(newPoint); if (axisModel.mainType === 'radiusAxis') { ret.pixel = newPoint[0] - oldPoint[0]; // ret.pixelLength = Math.abs(radiusExtent[1] - radiusExtent[0]); // ret.pixelStart = Math.min(radiusExtent[0], radiusExtent[1]); ret.pixelLength = radiusExtent[1] - radiusExtent[0]; ret.pixelStart = radiusExtent[0]; ret.signal = axis.inverse ? 1 : -1; } else { // 'angleAxis' ret.pixel = newPoint[1] - oldPoint[1]; // ret.pixelLength = Math.abs(angleExtent[1] - angleExtent[0]); // ret.pixelStart = Math.min(angleExtent[0], angleExtent[1]); ret.pixelLength = angleExtent[1] - angleExtent[0]; ret.pixelStart = angleExtent[0]; ret.signal = axis.inverse ? -1 : 1; } return ret; }, singleAxis: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var rect = coordInfo.model.coordinateSystem.getRect(); var ret = {}; oldPoint = oldPoint || [0, 0]; if (axis.orient === 'horizontal') { ret.pixel = newPoint[0] - oldPoint[0]; ret.pixelLength = rect.width; ret.pixelStart = rect.x; ret.signal = axis.inverse ? 1 : -1; } else { // 'vertical' ret.pixel = newPoint[1] - oldPoint[1]; ret.pixelLength = rect.height; ret.pixelStart = rect.y; ret.signal = axis.inverse ? -1 : 1; } return ret; } }; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Do not include './dataZoomSelect', // since it only work for toolbox dataZoom. /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var features = {}; function register$2(name, ctor) { features[name] = ctor; } function get$1(name) { return features[name]; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var ToolboxModel = extendComponentModel({ type: 'toolbox', layoutMode: { type: 'box', ignoreSize: true }, optionUpdated: function () { ToolboxModel.superApply(this, 'optionUpdated', arguments); each$1(this.option.feature, function (featureOpt, featureName) { var Feature = get$1(featureName); Feature && merge(featureOpt, Feature.defaultOption); }); }, defaultOption: { show: true, z: 6, zlevel: 0, orient: 'horizontal', left: 'right', top: 'top', // right // bottom backgroundColor: 'transparent', borderColor: '#ccc', borderRadius: 0, borderWidth: 0, padding: 5, itemSize: 15, itemGap: 8, showTitle: true, iconStyle: { borderColor: '#666', color: 'none' }, emphasis: { iconStyle: { borderColor: '#3E98C5' } }, // textStyle: {}, // feature tooltip: { show: false } } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ extendComponentView({ type: 'toolbox', render: function (toolboxModel, ecModel, api, payload) { var group = this.group; group.removeAll(); if (!toolboxModel.get('show')) { return; } var itemSize = +toolboxModel.get('itemSize'); var featureOpts = toolboxModel.get('feature') || {}; var features = this._features || (this._features = {}); var featureNames = []; each$1(featureOpts, function (opt, name) { featureNames.push(name); }); (new DataDiffer(this._featureNames || [], featureNames)) .add(processFeature) .update(processFeature) .remove(curry(processFeature, null)) .execute(); // Keep for diff. this._featureNames = featureNames; function processFeature(newIndex, oldIndex) { var featureName = featureNames[newIndex]; var oldName = featureNames[oldIndex]; var featureOpt = featureOpts[featureName]; var featureModel = new Model(featureOpt, toolboxModel, toolboxModel.ecModel); var feature; // FIX#11236, merge feature title from MagicType newOption. TODO: consider seriesIndex ? if (payload && payload.newTitle != null && payload.featureName === featureName) { featureOpt.title = payload.newTitle; } if (featureName && !oldName) { // Create if (isUserFeatureName(featureName)) { feature = { model: featureModel, onclick: featureModel.option.onclick, featureName: featureName }; } else { var Feature = get$1(featureName); if (!Feature) { return; } feature = new Feature(featureModel, ecModel, api); } features[featureName] = feature; } else { feature = features[oldName]; // If feature does not exsit. if (!feature) { return; } feature.model = featureModel; feature.ecModel = ecModel; feature.api = api; } if (!featureName && oldName) { feature.dispose && feature.dispose(ecModel, api); return; } if (!featureModel.get('show') || feature.unusable) { feature.remove && feature.remove(ecModel, api); return; } createIconPaths(featureModel, feature, featureName); featureModel.setIconStatus = function (iconName, status) { var option = this.option; var iconPaths = this.iconPaths; option.iconStatus = option.iconStatus || {}; option.iconStatus[iconName] = status; // FIXME iconPaths[iconName] && iconPaths[iconName].trigger(status); }; if (feature.render) { feature.render(featureModel, ecModel, api, payload); } } function createIconPaths(featureModel, feature, featureName) { var iconStyleModel = featureModel.getModel('iconStyle'); var iconStyleEmphasisModel = featureModel.getModel('emphasis.iconStyle'); // If one feature has mutiple icon. they are orginaized as // { // icon: { // foo: '', // bar: '' // }, // title: { // foo: '', // bar: '' // } // } var icons = feature.getIcons ? feature.getIcons() : featureModel.get('icon'); var titles = featureModel.get('title') || {}; if (typeof icons === 'string') { var icon = icons; var title = titles; icons = {}; titles = {}; icons[featureName] = icon; titles[featureName] = title; } var iconPaths = featureModel.iconPaths = {}; each$1(icons, function (iconStr, iconName) { var path = createIcon( iconStr, {}, { x: -itemSize / 2, y: -itemSize / 2, width: itemSize, height: itemSize } ); path.setStyle(iconStyleModel.getItemStyle()); path.hoverStyle = iconStyleEmphasisModel.getItemStyle(); // Text position calculation path.setStyle({ text: titles[iconName], textAlign: iconStyleEmphasisModel.get('textAlign'), textBorderRadius: iconStyleEmphasisModel.get('textBorderRadius'), textPadding: iconStyleEmphasisModel.get('textPadding'), textFill: null }); var tooltipModel = toolboxModel.getModel('tooltip'); if (tooltipModel && tooltipModel.get('show')) { path.attr('tooltip', extend({ content: titles[iconName], formatter: tooltipModel.get('formatter', true) || function () { return titles[iconName]; }, formatterParams: { componentType: 'toolbox', name: iconName, title: titles[iconName], $vars: ['name', 'title'] }, position: tooltipModel.get('position', true) || 'bottom' }, tooltipModel.option)); } setHoverStyle(path); if (toolboxModel.get('showTitle')) { path.__title = titles[iconName]; path.on('mouseover', function () { // Should not reuse above hoverStyle, which might be modified. var hoverStyle = iconStyleEmphasisModel.getItemStyle(); var defaultTextPosition = toolboxModel.get('orient') === 'vertical' ? (toolboxModel.get('right') == null ? 'right' : 'left') : (toolboxModel.get('bottom') == null ? 'bottom' : 'top'); path.setStyle({ textFill: iconStyleEmphasisModel.get('textFill') || hoverStyle.fill || hoverStyle.stroke || '#000', textBackgroundColor: iconStyleEmphasisModel.get('textBackgroundColor'), textPosition: iconStyleEmphasisModel.get('textPosition') || defaultTextPosition }); }) .on('mouseout', function () { path.setStyle({ textFill: null, textBackgroundColor: null }); }); } path.trigger(featureModel.get('iconStatus.' + iconName) || 'normal'); group.add(path); path.on('click', bind( feature.onclick, feature, ecModel, api, iconName )); iconPaths[iconName] = path; }); } layout$2(group, toolboxModel, api); // Render background after group is layout // FIXME group.add(makeBackground(group.getBoundingRect(), toolboxModel)); // Adjust icon title positions to avoid them out of screen group.eachChild(function (icon) { var titleText = icon.__title; var hoverStyle = icon.hoverStyle; // May be background element if (hoverStyle && titleText) { var rect = getBoundingRect( titleText, makeFont(hoverStyle) ); var offsetX = icon.position[0] + group.position[0]; var offsetY = icon.position[1] + group.position[1] + itemSize; var needPutOnTop = false; if (offsetY + rect.height > api.getHeight()) { hoverStyle.textPosition = 'top'; needPutOnTop = true; } var topOffset = needPutOnTop ? (-5 - rect.height) : (itemSize + 8); if (offsetX + rect.width / 2 > api.getWidth()) { hoverStyle.textPosition = ['100%', topOffset]; hoverStyle.textAlign = 'right'; } else if (offsetX - rect.width / 2 < 0) { hoverStyle.textPosition = [0, topOffset]; hoverStyle.textAlign = 'left'; } } }); }, updateView: function (toolboxModel, ecModel, api, payload) { each$1(this._features, function (feature) { feature.updateView && feature.updateView(feature.model, ecModel, api, payload); }); }, // updateLayout: function (toolboxModel, ecModel, api, payload) { // zrUtil.each(this._features, function (feature) { // feature.updateLayout && feature.updateLayout(feature.model, ecModel, api, payload); // }); // }, remove: function (ecModel, api) { each$1(this._features, function (feature) { feature.remove && feature.remove(ecModel, api); }); this.group.removeAll(); }, dispose: function (ecModel, api) { each$1(this._features, function (feature) { feature.dispose && feature.dispose(ecModel, api); }); } }); function isUserFeatureName(featureName) { return featureName.indexOf('my') === 0; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* global Uint8Array */ var saveAsImageLang = lang.toolbox.saveAsImage; function SaveAsImage(model) { this.model = model; } SaveAsImage.defaultOption = { show: true, icon: 'M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0', title: saveAsImageLang.title, type: 'png', // Default use option.backgroundColor // backgroundColor: '#fff', connectedBackgroundColor: '#fff', name: '', excludeComponents: ['toolbox'], pixelRatio: 1, lang: saveAsImageLang.lang.slice() }; SaveAsImage.prototype.unusable = !env$1.canvasSupported; var proto$2 = SaveAsImage.prototype; proto$2.onclick = function (ecModel, api) { var model = this.model; var title = model.get('name') || ecModel.get('title.0.text') || 'echarts'; var isSvg = api.getZr().painter.getType() === 'svg'; var type = isSvg ? 'svg' : model.get('type', true) || 'png'; var url = api.getConnectedDataURL({ type: type, backgroundColor: model.get('backgroundColor', true) || ecModel.get('backgroundColor') || '#fff', connectedBackgroundColor: model.get('connectedBackgroundColor'), excludeComponents: model.get('excludeComponents'), pixelRatio: model.get('pixelRatio') }); // Chrome and Firefox if (typeof MouseEvent === 'function' && !env$1.browser.ie && !env$1.browser.edge) { var $a = document.createElement('a'); $a.download = title + '.' + type; $a.target = '_blank'; $a.href = url; var evt = new MouseEvent('click', { // some micro front-end framework, window maybe is a Proxy view: document.defaultView, bubbles: true, cancelable: false }); $a.dispatchEvent(evt); } // IE else { if (window.navigator.msSaveOrOpenBlob) { var bstr = atob(url.split(',')[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } var blob = new Blob([u8arr]); window.navigator.msSaveOrOpenBlob(blob, title + '.' + type); } else { var lang$$1 = model.get('lang'); var html = '' + '' + '' + ''; var tab = window.open(); tab.document.write(html); } } }; register$2( 'saveAsImage', SaveAsImage ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var magicTypeLang = lang.toolbox.magicType; var INNER_STACK_KEYWORD = '__ec_magicType_stack__'; function MagicType(model) { this.model = model; } MagicType.defaultOption = { show: true, type: [], // Icon group icon: { /* eslint-disable */ line: 'M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4', bar: 'M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7', stack: 'M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z' // jshint ignore:line /* eslint-enable */ }, // `line`, `bar`, `stack`, `tiled` title: clone(magicTypeLang.title), option: {}, seriesIndex: {} }; var proto$3 = MagicType.prototype; proto$3.getIcons = function () { var model = this.model; var availableIcons = model.get('icon'); var icons = {}; each$1(model.get('type'), function (type) { if (availableIcons[type]) { icons[type] = availableIcons[type]; } }); return icons; }; var seriesOptGenreator = { 'line': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'bar') { return merge({ id: seriesId, type: 'line', // Preserve data related option data: seriesModel.get('data'), stack: seriesModel.get('stack'), markPoint: seriesModel.get('markPoint'), markLine: seriesModel.get('markLine') }, model.get('option.line') || {}, true); } }, 'bar': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'line') { return merge({ id: seriesId, type: 'bar', // Preserve data related option data: seriesModel.get('data'), stack: seriesModel.get('stack'), markPoint: seriesModel.get('markPoint'), markLine: seriesModel.get('markLine') }, model.get('option.bar') || {}, true); } }, 'stack': function (seriesType, seriesId, seriesModel, model) { var isStack = seriesModel.get('stack') === INNER_STACK_KEYWORD; if (seriesType === 'line' || seriesType === 'bar') { model.setIconStatus('stack', isStack ? 'normal' : 'emphasis'); return merge({ id: seriesId, stack: isStack ? '' : INNER_STACK_KEYWORD }, model.get('option.stack') || {}, true); } } }; var radioTypes = [ ['line', 'bar'], ['stack'] ]; proto$3.onclick = function (ecModel, api, type) { var model = this.model; var seriesIndex = model.get('seriesIndex.' + type); // Not supported magicType if (!seriesOptGenreator[type]) { return; } var newOption = { series: [] }; var generateNewSeriesTypes = function (seriesModel) { var seriesType = seriesModel.subType; var seriesId = seriesModel.id; var newSeriesOpt = seriesOptGenreator[type]( seriesType, seriesId, seriesModel, model ); if (newSeriesOpt) { // PENDING If merge original option? defaults(newSeriesOpt, seriesModel.option); newOption.series.push(newSeriesOpt); } // Modify boundaryGap var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.type === 'cartesian2d' && (type === 'line' || type === 'bar')) { var categoryAxis = coordSys.getAxesByScale('ordinal')[0]; if (categoryAxis) { var axisDim = categoryAxis.dim; var axisType = axisDim + 'Axis'; var axisModel = ecModel.queryComponents({ mainType: axisType, index: seriesModel.get(name + 'Index'), id: seriesModel.get(name + 'Id') })[0]; var axisIndex = axisModel.componentIndex; newOption[axisType] = newOption[axisType] || []; for (var i = 0; i <= axisIndex; i++) { newOption[axisType][axisIndex] = newOption[axisType][axisIndex] || {}; } newOption[axisType][axisIndex].boundaryGap = type === 'bar'; } } }; each$1(radioTypes, function (radio) { if (indexOf(radio, type) >= 0) { each$1(radio, function (item) { model.setIconStatus(item, 'normal'); }); } }); model.setIconStatus(type, 'emphasis'); ecModel.eachComponent( { mainType: 'series', query: seriesIndex == null ? null : { seriesIndex: seriesIndex } }, generateNewSeriesTypes ); var newTitle; // Change title of stack if (type === 'stack') { var isStack = newOption.series && newOption.series[0] && newOption.series[0].stack === INNER_STACK_KEYWORD; newTitle = isStack ? merge({ stack: magicTypeLang.title.tiled }, magicTypeLang.title) : clone(magicTypeLang.title); } api.dispatchAction({ type: 'changeMagicType', currentType: type, newOption: newOption, newTitle: newTitle, featureName: 'magicType' }); }; registerAction({ type: 'changeMagicType', event: 'magicTypeChanged', update: 'prepareAndUpdate' }, function (payload, ecModel) { ecModel.mergeOption(payload.newOption); }); register$2('magicType', MagicType); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var dataViewLang = lang.toolbox.dataView; var BLOCK_SPLITER = new Array(60).join('-'); var ITEM_SPLITER = '\t'; /** * Group series into two types * 1. on category axis, like line, bar * 2. others, like scatter, pie * @param {module:echarts/model/Global} ecModel * @return {Object} * @inner */ function groupSeries(ecModel) { var seriesGroupByCategoryAxis = {}; var otherSeries = []; var meta = []; ecModel.eachRawSeries(function (seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys && (coordSys.type === 'cartesian2d' || coordSys.type === 'polar')) { var baseAxis = coordSys.getBaseAxis(); if (baseAxis.type === 'category') { var key = baseAxis.dim + '_' + baseAxis.index; if (!seriesGroupByCategoryAxis[key]) { seriesGroupByCategoryAxis[key] = { categoryAxis: baseAxis, valueAxis: coordSys.getOtherAxis(baseAxis), series: [] }; meta.push({ axisDim: baseAxis.dim, axisIndex: baseAxis.index }); } seriesGroupByCategoryAxis[key].series.push(seriesModel); } else { otherSeries.push(seriesModel); } } else { otherSeries.push(seriesModel); } }); return { seriesGroupByCategoryAxis: seriesGroupByCategoryAxis, other: otherSeries, meta: meta }; } /** * Assemble content of series on cateogory axis * @param {Array.} series * @return {string} * @inner */ function assembleSeriesWithCategoryAxis(series) { var tables = []; each$1(series, function (group, key) { var categoryAxis = group.categoryAxis; var valueAxis = group.valueAxis; var valueAxisDim = valueAxis.dim; var headers = [' '].concat(map(group.series, function (series) { return series.name; })); var columns = [categoryAxis.model.getCategories()]; each$1(group.series, function (series) { var rawData = series.getRawData(); columns.push(series.getRawData().mapArray(rawData.mapDimension(valueAxisDim), function (val) { return val; })); }); // Assemble table content var lines = [headers.join(ITEM_SPLITER)]; for (var i = 0; i < columns[0].length; i++) { var items = []; for (var j = 0; j < columns.length; j++) { items.push(columns[j][i]); } lines.push(items.join(ITEM_SPLITER)); } tables.push(lines.join('\n')); }); return tables.join('\n\n' + BLOCK_SPLITER + '\n\n'); } /** * Assemble content of other series * @param {Array.} series * @return {string} * @inner */ function assembleOtherSeries(series) { return map(series, function (series) { var data = series.getRawData(); var lines = [series.name]; var vals = []; data.each(data.dimensions, function () { var argLen = arguments.length; var dataIndex = arguments[argLen - 1]; var name = data.getName(dataIndex); for (var i = 0; i < argLen - 1; i++) { vals[i] = arguments[i]; } lines.push((name ? (name + ITEM_SPLITER) : '') + vals.join(ITEM_SPLITER)); }); return lines.join('\n'); }).join('\n\n' + BLOCK_SPLITER + '\n\n'); } /** * @param {module:echarts/model/Global} * @return {Object} * @inner */ function getContentFromModel(ecModel) { var result = groupSeries(ecModel); return { value: filter([ assembleSeriesWithCategoryAxis(result.seriesGroupByCategoryAxis), assembleOtherSeries(result.other) ], function (str) { return str.replace(/[\n\t\s]/g, ''); }).join('\n\n' + BLOCK_SPLITER + '\n\n'), meta: result.meta }; } function trim$1(str) { return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } /** * If a block is tsv format */ function isTSVFormat(block) { // Simple method to find out if a block is tsv format var firstLine = block.slice(0, block.indexOf('\n')); if (firstLine.indexOf(ITEM_SPLITER) >= 0) { return true; } } var itemSplitRegex = new RegExp('[' + ITEM_SPLITER + ']+', 'g'); /** * @param {string} tsv * @return {Object} */ function parseTSVContents(tsv) { var tsvLines = tsv.split(/\n+/g); var headers = trim$1(tsvLines.shift()).split(itemSplitRegex); var categories = []; var series = map(headers, function (header) { return { name: header, data: [] }; }); for (var i = 0; i < tsvLines.length; i++) { var items = trim$1(tsvLines[i]).split(itemSplitRegex); categories.push(items.shift()); for (var j = 0; j < items.length; j++) { series[j] && (series[j].data[i] = items[j]); } } return { series: series, categories: categories }; } /** * @param {string} str * @return {Array.} * @inner */ function parseListContents(str) { var lines = str.split(/\n+/g); var seriesName = trim$1(lines.shift()); var data = []; for (var i = 0; i < lines.length; i++) { // if line is empty, ignore it. // there is a case that a user forgot to delete `\n`. var line = trim$1(lines[i]); if (!line) { continue; } var items = line.split(itemSplitRegex); var name = ''; var value; var hasName = false; if (isNaN(items[0])) { // First item is name hasName = true; name = items[0]; items = items.slice(1); data[i] = { name: name, value: [] }; value = data[i].value; } else { value = data[i] = []; } for (var j = 0; j < items.length; j++) { value.push(+items[j]); } if (value.length === 1) { hasName ? (data[i].value = value[0]) : (data[i] = value[0]); } } return { name: seriesName, data: data }; } /** * @param {string} str * @param {Array.} blockMetaList * @return {Object} * @inner */ function parseContents(str, blockMetaList) { var blocks = str.split(new RegExp('\n*' + BLOCK_SPLITER + '\n*', 'g')); var newOption = { series: [] }; each$1(blocks, function (block, idx) { if (isTSVFormat(block)) { var result = parseTSVContents(block); var blockMeta = blockMetaList[idx]; var axisKey = blockMeta.axisDim + 'Axis'; if (blockMeta) { newOption[axisKey] = newOption[axisKey] || []; newOption[axisKey][blockMeta.axisIndex] = { data: result.categories }; newOption.series = newOption.series.concat(result.series); } } else { var result = parseListContents(block); newOption.series.push(result); } }); return newOption; } /** * @alias {module:echarts/component/toolbox/feature/DataView} * @constructor * @param {module:echarts/model/Model} model */ function DataView(model) { this._dom = null; this.model = model; } DataView.defaultOption = { show: true, readOnly: false, optionToContent: null, contentToOption: null, icon: 'M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28', title: clone(dataViewLang.title), lang: clone(dataViewLang.lang), backgroundColor: '#fff', textColor: '#000', textareaColor: '#fff', textareaBorderColor: '#333', buttonColor: '#c23531', buttonTextColor: '#fff' }; DataView.prototype.onclick = function (ecModel, api) { var container = api.getDom(); var model = this.model; if (this._dom) { container.removeChild(this._dom); } var root = document.createElement('div'); root.style.cssText = 'position:absolute;left:5px;top:5px;bottom:5px;right:5px;'; root.style.backgroundColor = model.get('backgroundColor') || '#fff'; // Create elements var header = document.createElement('h4'); var lang$$1 = model.get('lang') || []; header.innerHTML = lang$$1[0] || model.get('title'); header.style.cssText = 'margin: 10px 20px;'; header.style.color = model.get('textColor'); var viewMain = document.createElement('div'); var textarea = document.createElement('textarea'); viewMain.style.cssText = 'display:block;width:100%;overflow:auto;'; var optionToContent = model.get('optionToContent'); var contentToOption = model.get('contentToOption'); var result = getContentFromModel(ecModel); if (typeof optionToContent === 'function') { var htmlOrDom = optionToContent(api.getOption()); if (typeof htmlOrDom === 'string') { viewMain.innerHTML = htmlOrDom; } else if (isDom(htmlOrDom)) { viewMain.appendChild(htmlOrDom); } } else { // Use default textarea viewMain.appendChild(textarea); textarea.readOnly = model.get('readOnly'); textarea.style.cssText = 'width:100%;height:100%;font-family:monospace;font-size:14px;line-height:1.6rem;'; textarea.style.color = model.get('textColor'); textarea.style.borderColor = model.get('textareaBorderColor'); textarea.style.backgroundColor = model.get('textareaColor'); textarea.value = result.value; } var blockMetaList = result.meta; var buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'position:absolute;bottom:0;left:0;right:0;'; var buttonStyle = 'float:right;margin-right:20px;border:none;' + 'cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px'; var closeButton = document.createElement('div'); var refreshButton = document.createElement('div'); buttonStyle += ';background-color:' + model.get('buttonColor'); buttonStyle += ';color:' + model.get('buttonTextColor'); var self = this; function close() { container.removeChild(root); self._dom = null; } addEventListener(closeButton, 'click', close); addEventListener(refreshButton, 'click', function () { var newOption; try { if (typeof contentToOption === 'function') { newOption = contentToOption(viewMain, api.getOption()); } else { newOption = parseContents(textarea.value, blockMetaList); } } catch (e) { close(); throw new Error('Data view format error ' + e); } if (newOption) { api.dispatchAction({ type: 'changeDataView', newOption: newOption }); } close(); }); closeButton.innerHTML = lang$$1[1]; refreshButton.innerHTML = lang$$1[2]; refreshButton.style.cssText = buttonStyle; closeButton.style.cssText = buttonStyle; !model.get('readOnly') && buttonContainer.appendChild(refreshButton); buttonContainer.appendChild(closeButton); root.appendChild(header); root.appendChild(viewMain); root.appendChild(buttonContainer); viewMain.style.height = (container.clientHeight - 80) + 'px'; container.appendChild(root); this._dom = root; }; DataView.prototype.remove = function (ecModel, api) { this._dom && api.getDom().removeChild(this._dom); }; DataView.prototype.dispose = function (ecModel, api) { this.remove(ecModel, api); }; /** * @inner */ function tryMergeDataOption(newData, originalData) { return map(newData, function (newVal, idx) { var original = originalData && originalData[idx]; if (isObject$1(original) && !isArray(original)) { var newValIsObject = isObject$1(newVal) && !isArray(newVal); if (!newValIsObject) { newVal = { value: newVal }; } // original data has name but new data has no name var shouldDeleteName = original.name != null && newVal.name == null; // Original data has option newVal = defaults(newVal, original); shouldDeleteName && (delete newVal.name); return newVal; } else { return newVal; } }); } register$2('dataView', DataView); registerAction({ type: 'changeDataView', event: 'dataViewChanged', update: 'prepareAndUpdate' }, function (payload, ecModel) { var newSeriesOptList = []; each$1(payload.newOption.series, function (seriesOpt) { var seriesModel = ecModel.getSeriesByName(seriesOpt.name)[0]; if (!seriesModel) { // New created series // Geuss the series type newSeriesOptList.push(extend({ // Default is scatter type: 'scatter' }, seriesOpt)); } else { var originalData = seriesModel.get('data'); newSeriesOptList.push({ name: seriesOpt.name, data: tryMergeDataOption(seriesOpt.data, originalData) }); } }); ecModel.mergeOption(defaults({ series: newSeriesOptList }, payload.newOption)); }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var curry$5 = curry; var each$16 = each$1; var map$2 = map; var mathMin$5 = Math.min; var mathMax$5 = Math.max; var mathPow$2 = Math.pow; var COVER_Z = 10000; var UNSELECT_THRESHOLD = 6; var MIN_RESIZE_LINE_WIDTH = 6; var MUTEX_RESOURCE_KEY = 'globalPan'; var DIRECTION_MAP = { w: [0, 0], e: [0, 1], n: [1, 0], s: [1, 1] }; var CURSOR_MAP = { w: 'ew', e: 'ew', n: 'ns', s: 'ns', ne: 'nesw', sw: 'nesw', nw: 'nwse', se: 'nwse' }; var DEFAULT_BRUSH_OPT = { brushStyle: { lineWidth: 2, stroke: 'rgba(0,0,0,0.3)', fill: 'rgba(0,0,0,0.1)' }, transformable: true, brushMode: 'single', removeOnClick: false }; var baseUID = 0; /** * @alias module:echarts/component/helper/BrushController * @constructor * @mixin {module:zrender/mixin/Eventful} * @event module:echarts/component/helper/BrushController#brush * params: * areas: Array., coord relates to container group, * If no container specified, to global. * opt { * isEnd: boolean, * removeOnClick: boolean * } * * @param {module:zrender/zrender~ZRender} zr */ function BrushController(zr) { if (__DEV__) { assert$1(zr); } Eventful.call(this); /** * @type {module:zrender/zrender~ZRender} * @private */ this._zr = zr; /** * @type {module:zrender/container/Group} * @readOnly */ this.group = new Group(); /** * Only for drawing (after enabledBrush). * 'line', 'rect', 'polygon' or false * If passing false/null/undefined, disable brush. * If passing 'auto', determined by panel.defaultBrushType * @private * @type {string} */ this._brushType; /** * Only for drawing (after enabledBrush). * * @private * @type {Object} */ this._brushOption; /** * @private * @type {Object} */ this._panels; /** * @private * @type {Array.} */ this._track = []; /** * @private * @type {boolean} */ this._dragging; /** * @private * @type {Array} */ this._covers = []; /** * @private * @type {moudule:zrender/container/Group} */ this._creatingCover; /** * `true` means global panel * @private * @type {module:zrender/container/Group|boolean} */ this._creatingPanel; /** * @private * @type {boolean} */ this._enableGlobalPan; /** * @private * @type {boolean} */ if (__DEV__) { this._mounted; } /** * @private * @type {string} */ this._uid = 'brushController_' + baseUID++; /** * @private * @type {Object} */ this._handlers = {}; each$16(pointerHandlers, function (handler, eventName) { this._handlers[eventName] = bind(handler, this); }, this); } BrushController.prototype = { constructor: BrushController, /** * If set to null/undefined/false, select disabled. * @param {Object} brushOption * @param {string|boolean} brushOption.brushType 'line', 'rect', 'polygon' or false * If passing false/null/undefined, disable brush. * If passing 'auto', determined by panel.defaultBrushType. * ('auto' can not be used in global panel) * @param {number} [brushOption.brushMode='single'] 'single' or 'multiple' * @param {boolean} [brushOption.transformable=true] * @param {boolean} [brushOption.removeOnClick=false] * @param {Object} [brushOption.brushStyle] * @param {number} [brushOption.brushStyle.width] * @param {number} [brushOption.brushStyle.lineWidth] * @param {string} [brushOption.brushStyle.stroke] * @param {string} [brushOption.brushStyle.fill] * @param {number} [brushOption.z] */ enableBrush: function (brushOption) { if (__DEV__) { assert$1(this._mounted); } this._brushType && doDisableBrush(this); brushOption.brushType && doEnableBrush(this, brushOption); return this; }, /** * @param {Array.} panelOpts If not pass, it is global brush. * Each items: { * panelId, // mandatory. * clipPath, // mandatory. function. * isTargetByCursor, // mandatory. function. * defaultBrushType, // optional, only used when brushType is 'auto'. * getLinearBrushOtherExtent, // optional. function. * } */ setPanels: function (panelOpts) { if (panelOpts && panelOpts.length) { var panels = this._panels = {}; each$1(panelOpts, function (panelOpts) { panels[panelOpts.panelId] = clone(panelOpts); }); } else { this._panels = null; } return this; }, /** * @param {Object} [opt] * @return {boolean} [opt.enableGlobalPan=false] */ mount: function (opt) { opt = opt || {}; if (__DEV__) { this._mounted = true; // should be at first. } this._enableGlobalPan = opt.enableGlobalPan; var thisGroup = this.group; this._zr.add(thisGroup); thisGroup.attr({ position: opt.position || [0, 0], rotation: opt.rotation || 0, scale: opt.scale || [1, 1] }); this._transform = thisGroup.getLocalTransform(); return this; }, eachCover: function (cb, context) { each$16(this._covers, cb, context); }, /** * Update covers. * @param {Array.} brushOptionList Like: * [ * {id: 'xx', brushType: 'line', range: [23, 44], brushStyle, transformable}, * {id: 'yy', brushType: 'rect', range: [[23, 44], [23, 54]]}, * ... * ] * `brushType` is required in each cover info. (can not be 'auto') * `id` is not mandatory. * `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default. * If brushOptionList is null/undefined, all covers removed. */ updateCovers: function (brushOptionList) { if (__DEV__) { assert$1(this._mounted); } brushOptionList = map(brushOptionList, function (brushOption) { return merge(clone(DEFAULT_BRUSH_OPT), brushOption, true); }); var tmpIdPrefix = '\0-brush-index-'; var oldCovers = this._covers; var newCovers = this._covers = []; var controller = this; var creatingCover = this._creatingCover; (new DataDiffer(oldCovers, brushOptionList, oldGetKey, getKey)) .add(addOrUpdate) .update(addOrUpdate) .remove(remove) .execute(); return this; function getKey(brushOption, index) { return (brushOption.id != null ? brushOption.id : tmpIdPrefix + index) + '-' + brushOption.brushType; } function oldGetKey(cover, index) { return getKey(cover.__brushOption, index); } function addOrUpdate(newIndex, oldIndex) { var newBrushOption = brushOptionList[newIndex]; // Consider setOption in event listener of brushSelect, // where updating cover when creating should be forbiden. if (oldIndex != null && oldCovers[oldIndex] === creatingCover) { newCovers[newIndex] = oldCovers[oldIndex]; } else { var cover = newCovers[newIndex] = oldIndex != null ? ( oldCovers[oldIndex].__brushOption = newBrushOption, oldCovers[oldIndex] ) : endCreating(controller, createCover(controller, newBrushOption)); updateCoverAfterCreation(controller, cover); } } function remove(oldIndex) { if (oldCovers[oldIndex] !== creatingCover) { controller.group.remove(oldCovers[oldIndex]); } } }, unmount: function () { if (__DEV__) { if (!this._mounted) { return; } } this.enableBrush(false); // container may 'removeAll' outside. clearCovers(this); this._zr.remove(this.group); if (__DEV__) { this._mounted = false; // should be at last. } return this; }, dispose: function () { this.unmount(); this.off(); } }; mixin(BrushController, Eventful); function doEnableBrush(controller, brushOption) { var zr = controller._zr; // Consider roam, which takes globalPan too. if (!controller._enableGlobalPan) { take(zr, MUTEX_RESOURCE_KEY, controller._uid); } mountHandlers(zr, controller._handlers); controller._brushType = brushOption.brushType; controller._brushOption = merge(clone(DEFAULT_BRUSH_OPT), brushOption, true); } function doDisableBrush(controller) { var zr = controller._zr; release(zr, MUTEX_RESOURCE_KEY, controller._uid); unmountHandlers(zr, controller._handlers); controller._brushType = controller._brushOption = null; } function mountHandlers(zr, handlers) { each$16(handlers, function (handler, eventName) { zr.on(eventName, handler); }); } function unmountHandlers(zr, handlers) { each$16(handlers, function (handler, eventName) { zr.off(eventName, handler); }); } function createCover(controller, brushOption) { var cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption); cover.__brushOption = brushOption; updateZ$1(cover, brushOption); controller.group.add(cover); return cover; } function endCreating(controller, creatingCover) { var coverRenderer = getCoverRenderer(creatingCover); if (coverRenderer.endCreating) { coverRenderer.endCreating(controller, creatingCover); updateZ$1(creatingCover, creatingCover.__brushOption); } return creatingCover; } function updateCoverShape(controller, cover) { var brushOption = cover.__brushOption; getCoverRenderer(cover).updateCoverShape( controller, cover, brushOption.range, brushOption ); } function updateZ$1(cover, brushOption) { var z = brushOption.z; z == null && (z = COVER_Z); cover.traverse(function (el) { el.z = z; el.z2 = z; // Consider in given container. }); } function updateCoverAfterCreation(controller, cover) { getCoverRenderer(cover).updateCommon(controller, cover); updateCoverShape(controller, cover); } function getCoverRenderer(cover) { return coverRenderers[cover.__brushOption.brushType]; } // return target panel or `true` (means global panel) function getPanelByPoint(controller, e, localCursorPoint) { var panels = controller._panels; if (!panels) { return true; // Global panel } var panel; var transform = controller._transform; each$16(panels, function (pn) { pn.isTargetByCursor(e, localCursorPoint, transform) && (panel = pn); }); return panel; } // Return a panel or true function getPanelByCover(controller, cover) { var panels = controller._panels; if (!panels) { return true; // Global panel } var panelId = cover.__brushOption.panelId; // User may give cover without coord sys info, // which is then treated as global panel. return panelId != null ? panels[panelId] : true; } function clearCovers(controller) { var covers = controller._covers; var originalLength = covers.length; each$16(covers, function (cover) { controller.group.remove(cover); }, controller); covers.length = 0; return !!originalLength; } function trigger$1(controller, opt) { var areas = map$2(controller._covers, function (cover) { var brushOption = cover.__brushOption; var range = clone(brushOption.range); return { brushType: brushOption.brushType, panelId: brushOption.panelId, range: range }; }); controller.trigger('brush', areas, { isEnd: !!opt.isEnd, removeOnClick: !!opt.removeOnClick }); } function shouldShowCover(controller) { var track = controller._track; if (!track.length) { return false; } var p2 = track[track.length - 1]; var p1 = track[0]; var dx = p2[0] - p1[0]; var dy = p2[1] - p1[1]; var dist = mathPow$2(dx * dx + dy * dy, 0.5); return dist > UNSELECT_THRESHOLD; } function getTrackEnds(track) { var tail = track.length - 1; tail < 0 && (tail = 0); return [track[0], track[tail]]; } function createBaseRectCover(doDrift, controller, brushOption, edgeNames) { var cover = new Group(); cover.add(new Rect({ name: 'main', style: makeStyle(brushOption), silent: true, draggable: true, cursor: 'move', drift: curry$5(doDrift, controller, cover, 'nswe'), ondragend: curry$5(trigger$1, controller, {isEnd: true}) })); each$16( edgeNames, function (name) { cover.add(new Rect({ name: name, style: {opacity: 0}, draggable: true, silent: true, invisible: true, drift: curry$5(doDrift, controller, cover, name), ondragend: curry$5(trigger$1, controller, {isEnd: true}) })); } ); return cover; } function updateBaseRect(controller, cover, localRange, brushOption) { var lineWidth = brushOption.brushStyle.lineWidth || 0; var handleSize = mathMax$5(lineWidth, MIN_RESIZE_LINE_WIDTH); var x = localRange[0][0]; var y = localRange[1][0]; var xa = x - lineWidth / 2; var ya = y - lineWidth / 2; var x2 = localRange[0][1]; var y2 = localRange[1][1]; var x2a = x2 - handleSize + lineWidth / 2; var y2a = y2 - handleSize + lineWidth / 2; var width = x2 - x; var height = y2 - y; var widtha = width + lineWidth; var heighta = height + lineWidth; updateRectShape(controller, cover, 'main', x, y, width, height); if (brushOption.transformable) { updateRectShape(controller, cover, 'w', xa, ya, handleSize, heighta); updateRectShape(controller, cover, 'e', x2a, ya, handleSize, heighta); updateRectShape(controller, cover, 'n', xa, ya, widtha, handleSize); updateRectShape(controller, cover, 's', xa, y2a, widtha, handleSize); updateRectShape(controller, cover, 'nw', xa, ya, handleSize, handleSize); updateRectShape(controller, cover, 'ne', x2a, ya, handleSize, handleSize); updateRectShape(controller, cover, 'sw', xa, y2a, handleSize, handleSize); updateRectShape(controller, cover, 'se', x2a, y2a, handleSize, handleSize); } } function updateCommon(controller, cover) { var brushOption = cover.__brushOption; var transformable = brushOption.transformable; var mainEl = cover.childAt(0); mainEl.useStyle(makeStyle(brushOption)); mainEl.attr({ silent: !transformable, cursor: transformable ? 'move' : 'default' }); each$16( ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw'], function (name) { var el = cover.childOfName(name); var globalDir = getGlobalDirection(controller, name); el && el.attr({ silent: !transformable, invisible: !transformable, cursor: transformable ? CURSOR_MAP[globalDir] + '-resize' : null }); } ); } function updateRectShape(controller, cover, name, x, y, w, h) { var el = cover.childOfName(name); el && el.setShape(pointsToRect( clipByPanel(controller, cover, [[x, y], [x + w, y + h]]) )); } function makeStyle(brushOption) { return defaults({strokeNoScale: true}, brushOption.brushStyle); } function formatRectRange(x, y, x2, y2) { var min = [mathMin$5(x, x2), mathMin$5(y, y2)]; var max = [mathMax$5(x, x2), mathMax$5(y, y2)]; return [ [min[0], max[0]], // x range [min[1], max[1]] // y range ]; } function getTransform$1(controller) { return getTransform(controller.group); } function getGlobalDirection(controller, localDirection) { if (localDirection.length > 1) { localDirection = localDirection.split(''); var globalDir = [ getGlobalDirection(controller, localDirection[0]), getGlobalDirection(controller, localDirection[1]) ]; (globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse(); return globalDir.join(''); } else { var map$$1 = {w: 'left', e: 'right', n: 'top', s: 'bottom'}; var inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'}; var globalDir = transformDirection( map$$1[localDirection], getTransform$1(controller) ); return inverseMap[globalDir]; } } function driftRect(toRectRange, fromRectRange, controller, cover, name, dx, dy, e) { var brushOption = cover.__brushOption; var rectRange = toRectRange(brushOption.range); var localDelta = toLocalDelta(controller, dx, dy); each$16(name.split(''), function (namePart) { var ind = DIRECTION_MAP[namePart]; rectRange[ind[0]][ind[1]] += localDelta[ind[0]]; }); brushOption.range = fromRectRange(formatRectRange( rectRange[0][0], rectRange[1][0], rectRange[0][1], rectRange[1][1] )); updateCoverAfterCreation(controller, cover); trigger$1(controller, {isEnd: false}); } function driftPolygon(controller, cover, dx, dy, e) { var range = cover.__brushOption.range; var localDelta = toLocalDelta(controller, dx, dy); each$16(range, function (point) { point[0] += localDelta[0]; point[1] += localDelta[1]; }); updateCoverAfterCreation(controller, cover); trigger$1(controller, {isEnd: false}); } function toLocalDelta(controller, dx, dy) { var thisGroup = controller.group; var localD = thisGroup.transformCoordToLocal(dx, dy); var localZero = thisGroup.transformCoordToLocal(0, 0); return [localD[0] - localZero[0], localD[1] - localZero[1]]; } function clipByPanel(controller, cover, data) { var panel = getPanelByCover(controller, cover); return (panel && panel !== true) ? panel.clipPath(data, controller._transform) : clone(data); } function pointsToRect(points) { var xmin = mathMin$5(points[0][0], points[1][0]); var ymin = mathMin$5(points[0][1], points[1][1]); var xmax = mathMax$5(points[0][0], points[1][0]); var ymax = mathMax$5(points[0][1], points[1][1]); return { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; } function resetCursor(controller, e, localCursorPoint) { if ( // Check active !controller._brushType // resetCursor should be always called when mouse is in zr area, // but not called when mouse is out of zr area to avoid bad influence // if `mousemove`, `mouseup` are triggered from `document` event. || isOutsideZrArea(controller, e) ) { return; } var zr = controller._zr; var covers = controller._covers; var currPanel = getPanelByPoint(controller, e, localCursorPoint); // Check whether in covers. if (!controller._dragging) { for (var i = 0; i < covers.length; i++) { var brushOption = covers[i].__brushOption; if (currPanel && (currPanel === true || brushOption.panelId === currPanel.panelId) && coverRenderers[brushOption.brushType].contain( covers[i], localCursorPoint[0], localCursorPoint[1] ) ) { // Use cursor style set on cover. return; } } } currPanel && zr.setCursorStyle('crosshair'); } function preventDefault(e) { var rawE = e.event; rawE.preventDefault && rawE.preventDefault(); } function mainShapeContain(cover, x, y) { return cover.childOfName('main').contain(x, y); } function updateCoverByMouse(controller, e, localCursorPoint, isEnd) { var creatingCover = controller._creatingCover; var panel = controller._creatingPanel; var thisBrushOption = controller._brushOption; var eventParams; controller._track.push(localCursorPoint.slice()); if (shouldShowCover(controller) || creatingCover) { if (panel && !creatingCover) { thisBrushOption.brushMode === 'single' && clearCovers(controller); var brushOption = clone(thisBrushOption); brushOption.brushType = determineBrushType(brushOption.brushType, panel); brushOption.panelId = panel === true ? null : panel.panelId; creatingCover = controller._creatingCover = createCover(controller, brushOption); controller._covers.push(creatingCover); } if (creatingCover) { var coverRenderer = coverRenderers[determineBrushType(controller._brushType, panel)]; var coverBrushOption = creatingCover.__brushOption; coverBrushOption.range = coverRenderer.getCreatingRange( clipByPanel(controller, creatingCover, controller._track) ); if (isEnd) { endCreating(controller, creatingCover); coverRenderer.updateCommon(controller, creatingCover); } updateCoverShape(controller, creatingCover); eventParams = {isEnd: isEnd}; } } else if ( isEnd && thisBrushOption.brushMode === 'single' && thisBrushOption.removeOnClick ) { // Help user to remove covers easily, only by a tiny drag, in 'single' mode. // But a single click do not clear covers, because user may have casual // clicks (for example, click on other component and do not expect covers // disappear). // Only some cover removed, trigger action, but not every click trigger action. if (getPanelByPoint(controller, e, localCursorPoint) && clearCovers(controller)) { eventParams = {isEnd: isEnd, removeOnClick: true}; } } return eventParams; } function determineBrushType(brushType, panel) { if (brushType === 'auto') { if (__DEV__) { assert$1( panel && panel.defaultBrushType, 'MUST have defaultBrushType when brushType is "atuo"' ); } return panel.defaultBrushType; } return brushType; } var pointerHandlers = { mousedown: function (e) { if (this._dragging) { // In case some browser do not support globalOut, // and release mose out side the browser. handleDragEnd(this, e); } else if (!e.target || !e.target.draggable) { preventDefault(e); var localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY); this._creatingCover = null; var panel = this._creatingPanel = getPanelByPoint(this, e, localCursorPoint); if (panel) { this._dragging = true; this._track = [localCursorPoint.slice()]; } } }, mousemove: function (e) { var x = e.offsetX; var y = e.offsetY; var localCursorPoint = this.group.transformCoordToLocal(x, y); resetCursor(this, e, localCursorPoint); if (this._dragging) { preventDefault(e); var eventParams = updateCoverByMouse(this, e, localCursorPoint, false); eventParams && trigger$1(this, eventParams); } }, mouseup: function (e) { handleDragEnd(this, e); } }; function handleDragEnd(controller, e) { if (controller._dragging) { preventDefault(e); var x = e.offsetX; var y = e.offsetY; var localCursorPoint = controller.group.transformCoordToLocal(x, y); var eventParams = updateCoverByMouse(controller, e, localCursorPoint, true); controller._dragging = false; controller._track = []; controller._creatingCover = null; // trigger event shoule be at final, after procedure will be nested. eventParams && trigger$1(controller, eventParams); } } function isOutsideZrArea(controller, x, y) { var zr = controller._zr; return x < 0 || x > zr.getWidth() || y < 0 || y > zr.getHeight(); } /** * key: brushType * @type {Object} */ var coverRenderers = { lineX: getLineRenderer(0), lineY: getLineRenderer(1), rect: { createCover: function (controller, brushOption) { return createBaseRectCover( curry$5( driftRect, function (range) { return range; }, function (range) { return range; } ), controller, brushOption, ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw'] ); }, getCreatingRange: function (localTrack) { var ends = getTrackEnds(localTrack); return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]); }, updateCoverShape: function (controller, cover, localRange, brushOption) { updateBaseRect(controller, cover, localRange, brushOption); }, updateCommon: updateCommon, contain: mainShapeContain }, polygon: { createCover: function (controller, brushOption) { var cover = new Group(); // Do not use graphic.Polygon because graphic.Polyline do not close the // border of the shape when drawing, which is a better experience for user. cover.add(new Polyline({ name: 'main', style: makeStyle(brushOption), silent: true })); return cover; }, getCreatingRange: function (localTrack) { return localTrack; }, endCreating: function (controller, cover) { cover.remove(cover.childAt(0)); // Use graphic.Polygon close the shape. cover.add(new Polygon({ name: 'main', draggable: true, drift: curry$5(driftPolygon, controller, cover), ondragend: curry$5(trigger$1, controller, {isEnd: true}) })); }, updateCoverShape: function (controller, cover, localRange, brushOption) { cover.childAt(0).setShape({ points: clipByPanel(controller, cover, localRange) }); }, updateCommon: updateCommon, contain: mainShapeContain } }; function getLineRenderer(xyIndex) { return { createCover: function (controller, brushOption) { return createBaseRectCover( curry$5( driftRect, function (range) { var rectRange = [range, [0, 100]]; xyIndex && rectRange.reverse(); return rectRange; }, function (rectRange) { return rectRange[xyIndex]; } ), controller, brushOption, [['w', 'e'], ['n', 's']][xyIndex] ); }, getCreatingRange: function (localTrack) { var ends = getTrackEnds(localTrack); var min = mathMin$5(ends[0][xyIndex], ends[1][xyIndex]); var max = mathMax$5(ends[0][xyIndex], ends[1][xyIndex]); return [min, max]; }, updateCoverShape: function (controller, cover, localRange, brushOption) { var otherExtent; // If brushWidth not specified, fit the panel. var panel = getPanelByCover(controller, cover); if (panel !== true && panel.getLinearBrushOtherExtent) { otherExtent = panel.getLinearBrushOtherExtent( xyIndex, controller._transform ); } else { var zr = controller._zr; otherExtent = [0, [zr.getWidth(), zr.getHeight()][1 - xyIndex]]; } var rectRange = [localRange, otherExtent]; xyIndex && rectRange.reverse(); updateBaseRect(controller, cover, rectRange, brushOption); }, updateCommon: updateCommon, contain: mainShapeContain }; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var IRRELEVANT_EXCLUDES = {'axisPointer': 1, 'tooltip': 1, 'brush': 1}; /** * Avoid that: mouse click on a elements that is over geo or graph, * but roam is triggered. */ function onIrrelevantElement(e, api, targetCoordSysModel) { var model = api.getComponentByElement(e.topTarget); // If model is axisModel, it works only if it is injected with coordinateSystem. var coordSys = model && model.coordinateSystem; return model && model !== targetCoordSysModel && !IRRELEVANT_EXCLUDES[model.mainType] && (coordSys && coordSys.model !== targetCoordSysModel); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function makeRectPanelClipPath(rect) { rect = normalizeRect(rect); return function (localPoints, transform) { return clipPointsByRect(localPoints, rect); }; } function makeLinearBrushOtherExtent(rect, specifiedXYIndex) { rect = normalizeRect(rect); return function (xyIndex) { var idx = specifiedXYIndex != null ? specifiedXYIndex : xyIndex; var brushWidth = idx ? rect.width : rect.height; var base = idx ? rect.x : rect.y; return [base, base + (brushWidth || 0)]; }; } function makeRectIsTargetByCursor(rect, api, targetModel) { rect = normalizeRect(rect); return function (e, localCursorPoint, transform) { return rect.contain(localCursorPoint[0], localCursorPoint[1]) && !onIrrelevantElement(e, api, targetModel); }; } // Consider width/height is negative. function normalizeRect(rect) { return BoundingRect.create(rect); } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$17 = each$1; var indexOf$2 = indexOf; var curry$6 = curry; var COORD_CONVERTS = ['dataToPoint', 'pointToData']; // FIXME // how to genarialize to more coordinate systems. var INCLUDE_FINDER_MAIN_TYPES = [ 'grid', 'xAxis', 'yAxis', 'geo', 'graph', 'polar', 'radiusAxis', 'angleAxis', 'bmap' ]; /** * [option in constructor]: * { * Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder. * } * * * [targetInfo]: * * There can be multiple axes in a single targetInfo. Consider the case * of `grid` component, a targetInfo represents a grid which contains one or more * cartesian and one or more axes. And consider the case of parallel system, * which has multiple axes in a coordinate system. * Can be { * panelId: ..., * coordSys: , * coordSyses: all cartesians. * gridModel: * xAxes: correspond to coordSyses on index * yAxes: correspond to coordSyses on index * } * or { * panelId: ..., * coordSys: * coordSyses: [] * geoModel: * } * * * [panelOpt]: * * Make from targetInfo. Input to BrushController. * { * panelId: ..., * rect: ... * } * * * [area]: * * Generated by BrushController or user input. * { * panelId: Used to locate coordInfo directly. If user inpput, no panelId. * brushType: determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y'). * Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder. * range: pixel range. * coordRange: representitive coord range (the first one of coordRanges). * coordRanges: coord ranges, used in multiple cartesian in one grid. * } */ /** * @param {Object} option contains Index/Id/Name of xAxis/yAxis/geo/grid * Each can be {number|Array.}. like: {xAxisIndex: [3, 4]} * @param {module:echarts/model/Global} ecModel * @param {Object} [opt] * @param {Array.} [opt.include] include coordinate system types. */ function BrushTargetManager(option, ecModel, opt) { /** * @private * @type {Array.} */ var targetInfoList = this._targetInfoList = []; var info = {}; var foundCpts = parseFinder$1(ecModel, option); each$17(targetInfoBuilders, function (builder, type) { if (!opt || !opt.include || indexOf$2(opt.include, type) >= 0) { builder(foundCpts, targetInfoList, info); } }); } var proto$5 = BrushTargetManager.prototype; proto$5.setOutputRanges = function (areas, ecModel) { this.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys) { (area.coordRanges || (area.coordRanges = [])).push(coordRange); // area.coordRange is the first of area.coordRanges if (!area.coordRange) { area.coordRange = coordRange; // In 'category' axis, coord to pixel is not reversible, so we can not // rebuild range by coordRange accrately, which may bring trouble when // brushing only one item. So we use __rangeOffset to rebuilding range // by coordRange. And this it only used in brush component so it is no // need to be adapted to coordRanges. var result = coordConvert[area.brushType](0, coordSys, coordRange); area.__rangeOffset = { offset: diffProcessor[area.brushType](result.values, area.range, [1, 1]), xyMinMax: result.xyMinMax }; } }); }; proto$5.matchOutputRanges = function (areas, ecModel, cb) { each$17(areas, function (area) { var targetInfo = this.findTargetInfo(area, ecModel); if (targetInfo && targetInfo !== true) { each$1( targetInfo.coordSyses, function (coordSys) { var result = coordConvert[area.brushType](1, coordSys, area.range); cb(area, result.values, coordSys, ecModel); } ); } }, this); }; proto$5.setInputRanges = function (areas, ecModel) { each$17(areas, function (area) { var targetInfo = this.findTargetInfo(area, ecModel); if (__DEV__) { assert$1( !targetInfo || targetInfo === true || area.coordRange, 'coordRange must be specified when coord index specified.' ); assert$1( !targetInfo || targetInfo !== true || area.range, 'range must be specified in global brush.' ); } area.range = area.range || []; // convert coordRange to global range and set panelId. if (targetInfo && targetInfo !== true) { area.panelId = targetInfo.panelId; // (1) area.range shoule always be calculate from coordRange but does // not keep its original value, for the sake of the dataZoom scenario, // where area.coordRange remains unchanged but area.range may be changed. // (2) Only support converting one coordRange to pixel range in brush // component. So do not consider `coordRanges`. // (3) About __rangeOffset, see comment above. var result = coordConvert[area.brushType](0, targetInfo.coordSys, area.coordRange); var rangeOffset = area.__rangeOffset; area.range = rangeOffset ? diffProcessor[area.brushType]( result.values, rangeOffset.offset, getScales(result.xyMinMax, rangeOffset.xyMinMax) ) : result.values; } }, this); }; proto$5.makePanelOpts = function (api, getDefaultBrushType) { return map(this._targetInfoList, function (targetInfo) { var rect = targetInfo.getPanelRect(); return { panelId: targetInfo.panelId, defaultBrushType: getDefaultBrushType && getDefaultBrushType(targetInfo), clipPath: makeRectPanelClipPath(rect), isTargetByCursor: makeRectIsTargetByCursor( rect, api, targetInfo.coordSysModel ), getLinearBrushOtherExtent: makeLinearBrushOtherExtent(rect) }; }); }; proto$5.controlSeries = function (area, seriesModel, ecModel) { // Check whether area is bound in coord, and series do not belong to that coord. // If do not do this check, some brush (like lineX) will controll all axes. var targetInfo = this.findTargetInfo(area, ecModel); return targetInfo === true || ( targetInfo && indexOf$2(targetInfo.coordSyses, seriesModel.coordinateSystem) >= 0 ); }; /** * If return Object, a coord found. * If reutrn true, global found. * Otherwise nothing found. * * @param {Object} area * @param {Array} targetInfoList * @return {Object|boolean} */ proto$5.findTargetInfo = function (area, ecModel) { var targetInfoList = this._targetInfoList; var foundCpts = parseFinder$1(ecModel, area); for (var i = 0; i < targetInfoList.length; i++) { var targetInfo = targetInfoList[i]; var areaPanelId = area.panelId; if (areaPanelId) { if (targetInfo.panelId === areaPanelId) { return targetInfo; } } else { for (var i = 0; i < targetInfoMatchers.length; i++) { if (targetInfoMatchers[i](foundCpts, targetInfo)) { return targetInfo; } } } } return true; }; function formatMinMax(minMax) { minMax[0] > minMax[1] && minMax.reverse(); return minMax; } function parseFinder$1(ecModel, option) { return parseFinder( ecModel, option, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES} ); } var targetInfoBuilders = { grid: function (foundCpts, targetInfoList) { var xAxisModels = foundCpts.xAxisModels; var yAxisModels = foundCpts.yAxisModels; var gridModels = foundCpts.gridModels; // Remove duplicated. var gridModelMap = createHashMap(); var xAxesHas = {}; var yAxesHas = {}; if (!xAxisModels && !yAxisModels && !gridModels) { return; } each$17(xAxisModels, function (axisModel) { var gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; }); each$17(yAxisModels, function (axisModel) { var gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); yAxesHas[gridModel.id] = true; }); each$17(gridModels, function (gridModel) { gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; yAxesHas[gridModel.id] = true; }); gridModelMap.each(function (gridModel) { var grid = gridModel.coordinateSystem; var cartesians = []; each$17(grid.getCartesians(), function (cartesian, index) { if (indexOf$2(xAxisModels, cartesian.getAxis('x').model) >= 0 || indexOf$2(yAxisModels, cartesian.getAxis('y').model) >= 0 ) { cartesians.push(cartesian); } }); targetInfoList.push({ panelId: 'grid--' + gridModel.id, gridModel: gridModel, coordSysModel: gridModel, // Use the first one as the representitive coordSys. coordSys: cartesians[0], coordSyses: cartesians, getPanelRect: panelRectBuilder.grid, xAxisDeclared: xAxesHas[gridModel.id], yAxisDeclared: yAxesHas[gridModel.id] }); }); }, geo: function (foundCpts, targetInfoList) { each$17(foundCpts.geoModels, function (geoModel) { var coordSys = geoModel.coordinateSystem; targetInfoList.push({ panelId: 'geo--' + geoModel.id, geoModel: geoModel, coordSysModel: geoModel, coordSys: coordSys, coordSyses: [coordSys], getPanelRect: panelRectBuilder.geo }); }); } }; var targetInfoMatchers = [ // grid function (foundCpts, targetInfo) { var xAxisModel = foundCpts.xAxisModel; var yAxisModel = foundCpts.yAxisModel; var gridModel = foundCpts.gridModel; !gridModel && xAxisModel && (gridModel = xAxisModel.axis.grid.model); !gridModel && yAxisModel && (gridModel = yAxisModel.axis.grid.model); return gridModel && gridModel === targetInfo.gridModel; }, // geo function (foundCpts, targetInfo) { var geoModel = foundCpts.geoModel; return geoModel && geoModel === targetInfo.geoModel; } ]; var panelRectBuilder = { grid: function () { // grid is not Transformable. return this.coordSys.grid.getRect().clone(); }, geo: function () { var coordSys = this.coordSys; var rect = coordSys.getBoundingRect().clone(); // geo roam and zoom transform rect.applyTransform(getTransform(coordSys)); return rect; } }; var coordConvert = { lineX: curry$6(axisConvert, 0), lineY: curry$6(axisConvert, 1), rect: function (to, coordSys, rangeOrCoordRange) { var xminymin = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]]); var xmaxymax = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]]); var values = [ formatMinMax([xminymin[0], xmaxymax[0]]), formatMinMax([xminymin[1], xmaxymax[1]]) ]; return {values: values, xyMinMax: values}; }, polygon: function (to, coordSys, rangeOrCoordRange) { var xyMinMax = [[Infinity, -Infinity], [Infinity, -Infinity]]; var values = map(rangeOrCoordRange, function (item) { var p = coordSys[COORD_CONVERTS[to]](item); xyMinMax[0][0] = Math.min(xyMinMax[0][0], p[0]); xyMinMax[1][0] = Math.min(xyMinMax[1][0], p[1]); xyMinMax[0][1] = Math.max(xyMinMax[0][1], p[0]); xyMinMax[1][1] = Math.max(xyMinMax[1][1], p[1]); return p; }); return {values: values, xyMinMax: xyMinMax}; } }; function axisConvert(axisNameIndex, to, coordSys, rangeOrCoordRange) { if (__DEV__) { assert$1( coordSys.type === 'cartesian2d', 'lineX/lineY brush is available only in cartesian2d.' ); } var axis = coordSys.getAxis(['x', 'y'][axisNameIndex]); var values = formatMinMax(map([0, 1], function (i) { return to ? axis.coordToData(axis.toLocalCoord(rangeOrCoordRange[i])) : axis.toGlobalCoord(axis.dataToCoord(rangeOrCoordRange[i])); })); var xyMinMax = []; xyMinMax[axisNameIndex] = values; xyMinMax[1 - axisNameIndex] = [NaN, NaN]; return {values: values, xyMinMax: xyMinMax}; } var diffProcessor = { lineX: curry$6(axisDiffProcessor, 0), lineY: curry$6(axisDiffProcessor, 1), rect: function (values, refer, scales) { return [ [values[0][0] - scales[0] * refer[0][0], values[0][1] - scales[0] * refer[0][1]], [values[1][0] - scales[1] * refer[1][0], values[1][1] - scales[1] * refer[1][1]] ]; }, polygon: function (values, refer, scales) { return map(values, function (item, idx) { return [item[0] - scales[0] * refer[idx][0], item[1] - scales[1] * refer[idx][1]]; }); } }; function axisDiffProcessor(axisNameIndex, values, refer, scales) { return [ values[0] - scales[axisNameIndex] * refer[0], values[1] - scales[axisNameIndex] * refer[1] ]; } // We have to process scale caused by dataZoom manually, // although it might be not accurate. function getScales(xyMinMaxCurr, xyMinMaxOrigin) { var sizeCurr = getSize(xyMinMaxCurr); var sizeOrigin = getSize(xyMinMaxOrigin); var scales = [sizeCurr[0] / sizeOrigin[0], sizeCurr[1] / sizeOrigin[1]]; isNaN(scales[0]) && (scales[0] = 1); isNaN(scales[1]) && (scales[1] = 1); return scales; } function getSize(xyMinMax) { return xyMinMax ? [xyMinMax[0][1] - xyMinMax[0][0], xyMinMax[1][1] - xyMinMax[1][0]] : [NaN, NaN]; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var each$18 = each$1; var ATTR$2 = '\0_ec_hist_store'; /** * @param {module:echarts/model/Global} ecModel * @param {Object} newSnapshot {dataZoomId, batch: [payloadInfo, ...]} */ function push(ecModel, newSnapshot) { var store = giveStore$1(ecModel); // If previous dataZoom can not be found, // complete an range with current range. each$18(newSnapshot, function (batchItem, dataZoomId) { var i = store.length - 1; for (; i >= 0; i--) { var snapshot = store[i]; if (snapshot[dataZoomId]) { break; } } if (i < 0) { // No origin range set, create one by current range. var dataZoomModel = ecModel.queryComponents( {mainType: 'dataZoom', subType: 'select', id: dataZoomId} )[0]; if (dataZoomModel) { var percentRange = dataZoomModel.getPercentRange(); store[0][dataZoomId] = { dataZoomId: dataZoomId, start: percentRange[0], end: percentRange[1] }; } } }); store.push(newSnapshot); } /** * @param {module:echarts/model/Global} ecModel * @return {Object} snapshot */ function pop(ecModel) { var store = giveStore$1(ecModel); var head = store[store.length - 1]; store.length > 1 && store.pop(); // Find top for all dataZoom. var snapshot = {}; each$18(head, function (batchItem, dataZoomId) { for (var i = store.length - 1; i >= 0; i--) { var batchItem = store[i][dataZoomId]; if (batchItem) { snapshot[dataZoomId] = batchItem; break; } } }); return snapshot; } /** * @param {module:echarts/model/Global} ecModel */ function clear$1(ecModel) { ecModel[ATTR$2] = null; } /** * @param {module:echarts/model/Global} ecModel * @return {number} records. always >= 1. */ function count(ecModel) { return giveStore$1(ecModel).length; } /** * [{key: dataZoomId, value: {dataZoomId, range}}, ...] * History length of each dataZoom may be different. * this._history[0] is used to store origin range. * @type {Array.} */ function giveStore$1(ecModel) { var store = ecModel[ATTR$2]; if (!store) { store = ecModel[ATTR$2] = [{}]; } return store; } /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ DataZoomModel.extend({ type: 'dataZoom.select' }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ DataZoomView.extend({ type: 'dataZoom.select' }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Only work for toolbox dataZoom. User * MUST NOT import this module directly. */ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Use dataZoomSelect var dataZoomLang = lang.toolbox.dataZoom; var each$15 = each$1; // Spectial component id start with \0ec\0, see echarts/model/Global.js~hasInnerId var DATA_ZOOM_ID_BASE = '\0_ec_\0toolbox-dataZoom_'; function DataZoom(model, ecModel, api) { /** * @private * @type {module:echarts/component/helper/BrushController} */ (this._brushController = new BrushController(api.getZr())) .on('brush', bind(this._onBrush, this)) .mount(); /** * @private * @type {boolean} */ this._isZoomActive; } DataZoom.defaultOption = { show: true, filterMode: 'filter', // Icon group icon: { zoom: 'M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1', back: 'M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26' }, // `zoom`, `back` title: clone(dataZoomLang.title), brushStyle: { borderWidth: 0, color: 'rgba(0,0,0,0.2)' } }; var proto$4 = DataZoom.prototype; proto$4.render = function (featureModel, ecModel, api, payload) { this.model = featureModel; this.ecModel = ecModel; this.api = api; updateZoomBtnStatus(featureModel, ecModel, this, payload, api); updateBackBtnStatus(featureModel, ecModel); }; proto$4.onclick = function (ecModel, api, type) { handlers[type].call(this); }; proto$4.remove = function (ecModel, api) { this._brushController.unmount(); }; proto$4.dispose = function (ecModel, api) { this._brushController.dispose(); }; /** * @private */ var handlers = { zoom: function () { var nextActive = !this._isZoomActive; this.api.dispatchAction({ type: 'takeGlobalCursor', key: 'dataZoomSelect', dataZoomSelectActive: nextActive }); }, back: function () { this._dispatchZoomAction(pop(this.ecModel)); } }; /** * @private */ proto$4._onBrush = function (areas, opt) { if (!opt.isEnd || !areas.length) { return; } var snapshot = {}; var ecModel = this.ecModel; this._brushController.updateCovers([]); // remove cover var brushTargetManager = new BrushTargetManager( retrieveAxisSetting(this.model.option), ecModel, {include: ['grid']} ); brushTargetManager.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys) { if (coordSys.type !== 'cartesian2d') { return; } var brushType = area.brushType; if (brushType === 'rect') { setBatch('x', coordSys, coordRange[0]); setBatch('y', coordSys, coordRange[1]); } else { setBatch(({lineX: 'x', lineY: 'y'})[brushType], coordSys, coordRange); } }); push(ecModel, snapshot); this._dispatchZoomAction(snapshot); function setBatch(dimName, coordSys, minMax) { var axis = coordSys.getAxis(dimName); var axisModel = axis.model; var dataZoomModel = findDataZoom(dimName, axisModel, ecModel); // Restrict range. var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan(); if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) { minMax = sliderMove( 0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan ); } dataZoomModel && (snapshot[dataZoomModel.id] = { dataZoomId: dataZoomModel.id, startValue: minMax[0], endValue: minMax[1] }); } function findDataZoom(dimName, axisModel, ecModel) { var found; ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, function (dzModel) { var has = dzModel.getAxisModel(dimName, axisModel.componentIndex); has && (found = dzModel); }); return found; } }; /** * @private */ proto$4._dispatchZoomAction = function (snapshot) { var batch = []; // Convert from hash map to array. each$15(snapshot, function (batchItem, dataZoomId) { batch.push(clone(batchItem)); }); batch.length && this.api.dispatchAction({ type: 'dataZoom', from: this.uid, batch: batch }); }; function retrieveAxisSetting(option) { var setting = {}; // Compatible with previous setting: null => all axis, false => no axis. each$1(['xAxisIndex', 'yAxisIndex'], function (name) { setting[name] = option[name]; setting[name] == null && (setting[name] = 'all'); (setting[name] === false || setting[name] === 'none') && (setting[name] = []); }); return setting; } function updateBackBtnStatus(featureModel, ecModel) { featureModel.setIconStatus( 'back', count(ecModel) > 1 ? 'emphasis' : 'normal' ); } function updateZoomBtnStatus(featureModel, ecModel, view, payload, api) { var zoomActive = view._isZoomActive; if (payload && payload.type === 'takeGlobalCursor') { zoomActive = payload.key === 'dataZoomSelect' ? payload.dataZoomSelectActive : false; } view._isZoomActive = zoomActive; featureModel.setIconStatus('zoom', zoomActive ? 'emphasis' : 'normal'); var brushTargetManager = new BrushTargetManager( retrieveAxisSetting(featureModel.option), ecModel, {include: ['grid']} ); view._brushController .setPanels(brushTargetManager.makePanelOpts(api, function (targetInfo) { return (targetInfo.xAxisDeclared && !targetInfo.yAxisDeclared) ? 'lineX' : (!targetInfo.xAxisDeclared && targetInfo.yAxisDeclared) ? 'lineY' : 'rect'; })) .enableBrush( zoomActive ? { brushType: 'auto', brushStyle: featureModel.getModel('brushStyle').getItemStyle() } : false ); } register$2('dataZoom', DataZoom); // Create special dataZoom option for select // FIXME consider the case of merge option, where axes options are not exists. registerPreprocessor(function (option) { if (!option) { return; } var dataZoomOpts = option.dataZoom || (option.dataZoom = []); if (!isArray(dataZoomOpts)) { option.dataZoom = dataZoomOpts = [dataZoomOpts]; } var toolboxOpt = option.toolbox; if (toolboxOpt) { // Assume there is only one toolbox if (isArray(toolboxOpt)) { toolboxOpt = toolboxOpt[0]; } if (toolboxOpt && toolboxOpt.feature) { var dataZoomOpt = toolboxOpt.feature.dataZoom; // FIXME: If add dataZoom when setOption in merge mode, // no axis info to be added. See `test/dataZoom-extreme.html` addForAxis('xAxis', dataZoomOpt); addForAxis('yAxis', dataZoomOpt); } } function addForAxis(axisName, dataZoomOpt) { if (!dataZoomOpt) { return; } // Try not to modify model, because it is not merged yet. var axisIndicesName = axisName + 'Index'; var givenAxisIndices = dataZoomOpt[axisIndicesName]; if (givenAxisIndices != null && givenAxisIndices !== 'all' && !isArray(givenAxisIndices) ) { givenAxisIndices = (givenAxisIndices === false || givenAxisIndices === 'none') ? [] : [givenAxisIndices]; } forEachComponent(axisName, function (axisOpt, axisIndex) { if (givenAxisIndices != null && givenAxisIndices !== 'all' && indexOf(givenAxisIndices, axisIndex) === -1 ) { return; } var newOpt = { type: 'select', $fromToolbox: true, // Default to be filter filterMode: dataZoomOpt.filterMode || 'filter', // Id for merge mapping. id: DATA_ZOOM_ID_BASE + axisName + axisIndex }; // FIXME // Only support one axis now. newOpt[axisIndicesName] = axisIndex; dataZoomOpts.push(newOpt); }); } function forEachComponent(mainType, cb) { var opts = option[mainType]; if (!isArray(opts)) { opts = opts ? [opts] : []; } each$15(opts, cb); } }); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var restoreLang = lang.toolbox.restore; function Restore(model) { this.model = model; } Restore.defaultOption = { show: true, /* eslint-disable */ icon: 'M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5', /* eslint-enable */ title: restoreLang.title }; var proto$6 = Restore.prototype; proto$6.onclick = function (ecModel, api, type) { clear$1(ecModel); api.dispatchAction({ type: 'restore', from: this.uid }); }; register$2('restore', Restore); registerAction( {type: 'restore', event: 'restore', update: 'prepareAndUpdate'}, function (payload, ecModel) { ecModel.resetOption('recreate'); } ); /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var urn = 'urn:schemas-microsoft-com:vml'; var win = typeof window === 'undefined' ? null : window; var vmlInited = false; var doc = win && win.document; function createNode(tagName) { return doCreateNode(tagName); } // Avoid assign to an exported variable, for transforming to cjs. var doCreateNode; if (doc && !env$1.canvasSupported) { try { !doc.namespaces.zrvml && doc.namespaces.add('zrvml', urn); doCreateNode = function (tagName) { return doc.createElement(''); }; } catch (e) { doCreateNode = function (tagName) { return doc.createElement('<' + tagName + ' xmlns="' + urn + '" class="zrvml">'); }; } } // From raphael function initVML() { if (vmlInited || !doc) { return; } vmlInited = true; var styleSheets = doc.styleSheets; if (styleSheets.length < 31) { doc.createStyleSheet().addRule('.zrvml', 'behavior:url(#default#VML)'); } else { // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx styleSheets[0].addRule('.zrvml', 'behavior:url(#default#VML)'); } } // http://www.w3.org/TR/NOTE-VML // TODO Use proxy like svg instead of overwrite brush methods var CMD$3 = PathProxy.CMD; var round$2 = Math.round; var sqrt = Math.sqrt; var abs$1 = Math.abs; var cos = Math.cos; var sin = Math.sin; var mathMax$6 = Math.max; if (!env$1.canvasSupported) { var comma = ','; var imageTransformPrefix = 'progid:DXImageTransform.Microsoft'; var Z = 21600; var Z2 = Z / 2; var ZLEVEL_BASE = 100000; var Z_BASE = 1000; var initRootElStyle = function (el) { el.style.cssText = 'position:absolute;left:0;top:0;width:1px;height:1px;'; el.coordsize = Z + ',' + Z; el.coordorigin = '0,0'; }; var encodeHtmlAttribute = function (s) { return String(s).replace(/&/g, '&').replace(/"/g, '"'); }; var rgb2Str = function (r, g, b) { return 'rgb(' + [r, g, b].join(',') + ')'; }; var append = function (parent, child) { if (child && parent && child.parentNode !== parent) { parent.appendChild(child); } }; var remove = function (parent, child) { if (child && parent && child.parentNode === parent) { parent.removeChild(child); } }; var getZIndex = function (zlevel, z, z2) { // z 的取值范围为 [0, 1000] return (parseFloat(zlevel) || 0) * ZLEVEL_BASE + (parseFloat(z) || 0) * Z_BASE + z2; }; var parsePercent$3 = parsePercent; /*************************************************** * PATH **************************************************/ var setColorAndOpacity = function (el, color, opacity) { var colorArr = parse(color); opacity = +opacity; if (isNaN(opacity)) { opacity = 1; } if (colorArr) { el.color = rgb2Str(colorArr[0], colorArr[1], colorArr[2]); el.opacity = opacity * colorArr[3]; } }; var getColorAndAlpha = function (color) { var colorArr = parse(color); return [ rgb2Str(colorArr[0], colorArr[1], colorArr[2]), colorArr[3] ]; }; var updateFillNode = function (el, style, zrEl) { // TODO pattern var fill = style.fill; if (fill != null) { // Modified from excanvas if (fill instanceof Gradient) { var gradientType; var angle = 0; var focus = [0, 0]; // additional offset var shift = 0; // scale factor for offset var expansion = 1; var rect = zrEl.getBoundingRect(); var rectWidth = rect.width; var rectHeight = rect.height; if (fill.type === 'linear') { gradientType = 'gradient'; var transform = zrEl.transform; var p0 = [fill.x * rectWidth, fill.y * rectHeight]; var p1 = [fill.x2 * rectWidth, fill.y2 * rectHeight]; if (transform) { applyTransform(p0, p0, transform); applyTransform(p1, p1, transform); } var dx = p1[0] - p0[0]; var dy = p1[1] - p0[1]; angle = Math.atan2(dx, dy) * 180 / Math.PI; // The angle should be a non-negative number. if (angle < 0) { angle += 360; } // Very small angles produce an unexpected result because they are // converted to a scientific notation string. if (angle < 1e-6) { angle = 0; } } else { gradientType = 'gradientradial'; var p0 = [fill.x * rectWidth, fill.y * rectHeight]; var transform = zrEl.transform; var scale$$1 = zrEl.scale; var width = rectWidth; var height = rectHeight; focus = [ // Percent in bounding rect (p0[0] - rect.x) / width, (p0[1] - rect.y) / height ]; if (transform) { applyTransform(p0, p0, transform); } width /= scale$$1[0] * Z; height /= scale$$1[1] * Z; var dimension = mathMax$6(width, height); shift = 2 * 0 / dimension; expansion = 2 * fill.r / dimension - shift; } // We need to sort the color stops in ascending order by offset, // otherwise IE won't interpret it correctly. var stops = fill.colorStops.slice(); stops.sort(function (cs1, cs2) { return cs1.offset - cs2.offset; }); var length$$1 = stops.length; // Color and alpha list of first and last stop var colorAndAlphaList = []; var colors = []; for (var i = 0; i < length$$1; i++) { var stop = stops[i]; var colorAndAlpha = getColorAndAlpha(stop.color); colors.push(stop.offset * expansion + shift + ' ' + colorAndAlpha[0]); if (i === 0 || i === length$$1 - 1) { colorAndAlphaList.push(colorAndAlpha); } } if (length$$1 >= 2) { var color1 = colorAndAlphaList[0][0]; var color2 = colorAndAlphaList[1][0]; var opacity1 = colorAndAlphaList[0][1] * style.opacity; var opacity2 = colorAndAlphaList[1][1] * style.opacity; el.type = gradientType; el.method = 'none'; el.focus = '100%'; el.angle = angle; el.color = color1; el.color2 = color2; el.colors = colors.join(','); // When colors attribute is used, the meanings of opacity and o:opacity2 // are reversed. el.opacity = opacity2; // FIXME g_o_:opacity ? el.opacity2 = opacity1; } if (gradientType === 'radial') { el.focusposition = focus.join(','); } } else { // FIXME Change from Gradient fill to color fill setColorAndOpacity(el, fill, style.opacity); } } }; var updateStrokeNode = function (el, style) { // if (style.lineJoin != null) { // el.joinstyle = style.lineJoin; // } // if (style.miterLimit != null) { // el.miterlimit = style.miterLimit * Z; // } // if (style.lineCap != null) { // el.endcap = style.lineCap; // } if (style.lineDash) { el.dashstyle = style.lineDash.join(' '); } if (style.stroke != null && !(style.stroke instanceof Gradient)) { setColorAndOpacity(el, style.stroke, style.opacity); } }; var updateFillAndStroke = function (vmlEl, type, style, zrEl) { var isFill = type === 'fill'; var el = vmlEl.getElementsByTagName(type)[0]; // Stroke must have lineWidth if (style[type] != null && style[type] !== 'none' && (isFill || (!isFill && style.lineWidth))) { vmlEl[isFill ? 'filled' : 'stroked'] = 'true'; // FIXME Remove before updating, or set `colors` will throw error if (style[type] instanceof Gradient) { remove(vmlEl, el); } if (!el) { el = createNode(type); } isFill ? updateFillNode(el, style, zrEl) : updateStrokeNode(el, style); append(vmlEl, el); } else { vmlEl[isFill ? 'filled' : 'stroked'] = 'false'; remove(vmlEl, el); } }; var points$1 = [[], [], []]; var pathDataToString = function (path, m) { var M = CMD$3.M; var C = CMD$3.C; var L = CMD$3.L; var A = CMD$3.A; var Q = CMD$3.Q; var str = []; var nPoint; var cmdStr; var cmd; var i; var xi; var yi; var data = path.data; var dataLength = path.len(); for (i = 0; i < dataLength;) { cmd = data[i++]; cmdStr = ''; nPoint = 0; switch (cmd) { case M: cmdStr = ' m '; nPoint = 1; xi = data[i++]; yi = data[i++]; points$1[0][0] = xi; points$1[0][1] = yi; break; case L: cmdStr = ' l '; nPoint = 1; xi = data[i++]; yi = data[i++]; points$1[0][0] = xi; points$1[0][1] = yi; break; case Q: case C: cmdStr = ' c '; nPoint = 3; var x1 = data[i++]; var y1 = data[i++]; var x2 = data[i++]; var y2 = data[i++]; var x3; var y3; if (cmd === Q) { // Convert quadratic to cubic using degree elevation x3 = x2; y3 = y2; x2 = (x2 + 2 * x1) / 3; y2 = (y2 + 2 * y1) / 3; x1 = (xi + 2 * x1) / 3; y1 = (yi + 2 * y1) / 3; } else { x3 = data[i++]; y3 = data[i++]; } points$1[0][0] = x1; points$1[0][1] = y1; points$1[1][0] = x2; points$1[1][1] = y2; points$1[2][0] = x3; points$1[2][1] = y3; xi = x3; yi = y3; break; case A: var x = 0; var y = 0; var sx = 1; var sy = 1; var angle = 0; if (m) { // Extract SRT from matrix x = m[4]; y = m[5]; sx = sqrt(m[0] * m[0] + m[1] * m[1]); sy = sqrt(m[2] * m[2] + m[3] * m[3]); angle = Math.atan2(-m[1] / sy, m[0] / sx); } var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var startAngle = data[i++] + angle; var endAngle = data[i++] + startAngle + angle; // FIXME // var psi = data[i++]; i++; var clockwise = data[i++]; var x0 = cx + cos(startAngle) * rx; var y0 = cy + sin(startAngle) * ry; var x1 = cx + cos(endAngle) * rx; var y1 = cy + sin(endAngle) * ry; var type = clockwise ? ' wa ' : ' at '; if (Math.abs(x0 - x1) < 1e-4) { // IE won't render arches drawn counter clockwise if x0 == x1. if (Math.abs(endAngle - startAngle) > 1e-2) { // Offset x0 by 1/80 of a pixel. Use something // that can be represented in binary if (clockwise) { x0 += 270 / Z; } } else { // Avoid case draw full circle if (Math.abs(y0 - cy) < 1e-4) { if ((clockwise && x0 < cx) || (!clockwise && x0 > cx)) { y1 -= 270 / Z; } else { y1 += 270 / Z; } } else if ((clockwise && y0 < cy) || (!clockwise && y0 > cy)) { x1 += 270 / Z; } else { x1 -= 270 / Z; } } } str.push( type, round$2(((cx - rx) * sx + x) * Z - Z2), comma, round$2(((cy - ry) * sy + y) * Z - Z2), comma, round$2(((cx + rx) * sx + x) * Z - Z2), comma, round$2(((cy + ry) * sy + y) * Z - Z2), comma, round$2((x0 * sx + x) * Z - Z2), comma, round$2((y0 * sy + y) * Z - Z2), comma, round$2((x1 * sx + x) * Z - Z2), comma, round$2((y1 * sy + y) * Z - Z2) ); xi = x1; yi = y1; break; case CMD$3.R: var p0 = points$1[0]; var p1 = points$1[1]; // x0, y0 p0[0] = data[i++]; p0[1] = data[i++]; // x1, y1 p1[0] = p0[0] + data[i++]; p1[1] = p0[1] + data[i++]; if (m) { applyTransform(p0, p0, m); applyTransform(p1, p1, m); } p0[0] = round$2(p0[0] * Z - Z2); p1[0] = round$2(p1[0] * Z - Z2); p0[1] = round$2(p0[1] * Z - Z2); p1[1] = round$2(p1[1] * Z - Z2); str.push( // x0, y0 ' m ', p0[0], comma, p0[1], // x1, y0 ' l ', p1[0], comma, p0[1], // x1, y1 ' l ', p1[0], comma, p1[1], // x0, y1 ' l ', p0[0], comma, p1[1] ); break; case CMD$3.Z: // FIXME Update xi, yi str.push(' x '); } if (nPoint > 0) { str.push(cmdStr); for (var k = 0; k < nPoint; k++) { var p = points$1[k]; m && applyTransform(p, p, m); // 不 round 会非常慢 str.push( round$2(p[0] * Z - Z2), comma, round$2(p[1] * Z - Z2), k < nPoint - 1 ? comma : '' ); } } } return str.join(''); }; // Rewrite the original path method Path.prototype.brushVML = function (vmlRoot) { var style = this.style; var vmlEl = this._vmlEl; if (!vmlEl) { vmlEl = createNode('shape'); initRootElStyle(vmlEl); this._vmlEl = vmlEl; } updateFillAndStroke(vmlEl, 'fill', style, this); updateFillAndStroke(vmlEl, 'stroke', style, this); var m = this.transform; var needTransform = m != null; var strokeEl = vmlEl.getElementsByTagName('stroke')[0]; if (strokeEl) { var lineWidth = style.lineWidth; // Get the line scale. // Determinant of this.m_ means how much the area is enlarged by the // transformation. So its square root can be used as a scale factor // for width. if (needTransform && !style.strokeNoScale) { var det = m[0] * m[3] - m[1] * m[2]; lineWidth *= sqrt(abs$1(det)); } strokeEl.weight = lineWidth + 'px'; } var path = this.path || (this.path = new PathProxy()); if (this.__dirtyPath) { path.beginPath(); path.subPixelOptimize = false; this.buildPath(path, this.shape); path.toStatic(); this.__dirtyPath = false; } vmlEl.path = pathDataToString(path, this.transform); vmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Append to root append(vmlRoot, vmlEl); // Text if (style.text != null) { this.drawRectText(vmlRoot, this.getBoundingRect()); } else { this.removeRectText(vmlRoot); } }; Path.prototype.onRemove = function (vmlRoot) { remove(vmlRoot, this._vmlEl); this.removeRectText(vmlRoot); }; Path.prototype.onAdd = function (vmlRoot) { append(vmlRoot, this._vmlEl); this.appendRectText(vmlRoot); }; /*************************************************** * IMAGE **************************************************/ var isImage = function (img) { // FIXME img instanceof Image 如果 img 是一个字符串的时候,IE8 下会报错 return (typeof img === 'object') && img.tagName && img.tagName.toUpperCase() === 'IMG'; // return img instanceof Image; }; // Rewrite the original path method ZImage.prototype.brushVML = function (vmlRoot) { var style = this.style; var image = style.image; // Image original width, height var ow; var oh; if (isImage(image)) { var src = image.src; if (src === this._imageSrc) { ow = this._imageWidth; oh = this._imageHeight; } else { var imageRuntimeStyle = image.runtimeStyle; var oldRuntimeWidth = imageRuntimeStyle.width; var oldRuntimeHeight = imageRuntimeStyle.height; imageRuntimeStyle.width = 'auto'; imageRuntimeStyle.height = 'auto'; // get the original size ow = image.width; oh = image.height; // and remove overides imageRuntimeStyle.width = oldRuntimeWidth; imageRuntimeStyle.height = oldRuntimeHeight; // Caching image original width, height and src this._imageSrc = src; this._imageWidth = ow; this._imageHeight = oh; } image = src; } else { if (image === this._imageSrc) { ow = this._imageWidth; oh = this._imageHeight; } } if (!image) { return; } var x = style.x || 0; var y = style.y || 0; var dw = style.width; var dh = style.height; var sw = style.sWidth; var sh = style.sHeight; var sx = style.sx || 0; var sy = style.sy || 0; var hasCrop = sw && sh; var vmlEl = this._vmlEl; if (!vmlEl) { // FIXME 使用 group 在 left, top 都不是 0 的时候就无法显示了。 // vmlEl = vmlCore.createNode('group'); vmlEl = doc.createElement('div'); initRootElStyle(vmlEl); this._vmlEl = vmlEl; } var vmlElStyle = vmlEl.style; var hasRotation = false; var m; var scaleX = 1; var scaleY = 1; if (this.transform) { m = this.transform; scaleX = sqrt(m[0] * m[0] + m[1] * m[1]); scaleY = sqrt(m[2] * m[2] + m[3] * m[3]); hasRotation = m[1] || m[2]; } if (hasRotation) { // If filters are necessary (rotation exists), create them // filters are bog-slow, so only create them if abbsolutely necessary // The following check doesn't account for skews (which don't exist // in the canvas spec (yet) anyway. // From excanvas var p0 = [x, y]; var p1 = [x + dw, y]; var p2 = [x, y + dh]; var p3 = [x + dw, y + dh]; applyTransform(p0, p0, m); applyTransform(p1, p1, m); applyTransform(p2, p2, m); applyTransform(p3, p3, m); var maxX = mathMax$6(p0[0], p1[0], p2[0], p3[0]); var maxY = mathMax$6(p0[1], p1[1], p2[1], p3[1]); var transformFilter = []; transformFilter.push('M11=', m[0] / scaleX, comma, 'M12=', m[2] / scaleY, comma, 'M21=', m[1] / scaleX, comma, 'M22=', m[3] / scaleY, comma, 'Dx=', round$2(x * scaleX + m[4]), comma, 'Dy=', round$2(y * scaleY + m[5])); vmlElStyle.padding = '0 ' + round$2(maxX) + 'px ' + round$2(maxY) + 'px 0'; // FIXME DXImageTransform 在 IE11 的兼容模式下不起作用 vmlElStyle.filter = imageTransformPrefix + '.Matrix(' + transformFilter.join('') + ', SizingMethod=clip)'; } else { if (m) { x = x * scaleX + m[4]; y = y * scaleY + m[5]; } vmlElStyle.filter = ''; vmlElStyle.left = round$2(x) + 'px'; vmlElStyle.top = round$2(y) + 'px'; } var imageEl = this._imageEl; var cropEl = this._cropEl; if (!imageEl) { imageEl = doc.createElement('div'); this._imageEl = imageEl; } var imageELStyle = imageEl.style; if (hasCrop) { // Needs know image original width and height if (!(ow && oh)) { var tmpImage = new Image(); var self = this; tmpImage.onload = function () { tmpImage.onload = null; ow = tmpImage.width; oh = tmpImage.height; // Adjust image width and height to fit the ratio destinationSize / sourceSize imageELStyle.width = round$2(scaleX * ow * dw / sw) + 'px'; imageELStyle.height = round$2(scaleY * oh * dh / sh) + 'px'; // Caching image original width, height and src self._imageWidth = ow; self._imageHeight = oh; self._imageSrc = image; }; tmpImage.src = image; } else { imageELStyle.width = round$2(scaleX * ow * dw / sw) + 'px'; imageELStyle.height = round$2(scaleY * oh * dh / sh) + 'px'; } if (!cropEl) { cropEl = doc.createElement('div'); cropEl.style.overflow = 'hidden'; this._cropEl = cropEl; } var cropElStyle = cropEl.style; cropElStyle.width = round$2((dw + sx * dw / sw) * scaleX); cropElStyle.height = round$2((dh + sy * dh / sh) * scaleY); cropElStyle.filter = imageTransformPrefix + '.Matrix(Dx=' + (-sx * dw / sw * scaleX) + ',Dy=' + (-sy * dh / sh * scaleY) + ')'; if (!cropEl.parentNode) { vmlEl.appendChild(cropEl); } if (imageEl.parentNode !== cropEl) { cropEl.appendChild(imageEl); } } else { imageELStyle.width = round$2(scaleX * dw) + 'px'; imageELStyle.height = round$2(scaleY * dh) + 'px'; vmlEl.appendChild(imageEl); if (cropEl && cropEl.parentNode) { vmlEl.removeChild(cropEl); this._cropEl = null; } } var filterStr = ''; var alpha = style.opacity; if (alpha < 1) { filterStr += '.Alpha(opacity=' + round$2(alpha * 100) + ') '; } filterStr += imageTransformPrefix + '.AlphaImageLoader(src=' + image + ', SizingMethod=scale)'; imageELStyle.filter = filterStr; vmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Append to root append(vmlRoot, vmlEl); // Text if (style.text != null) { this.drawRectText(vmlRoot, this.getBoundingRect()); } }; ZImage.prototype.onRemove = function (vmlRoot) { remove(vmlRoot, this._vmlEl); this._vmlEl = null; this._cropEl = null; this._imageEl = null; this.removeRectText(vmlRoot); }; ZImage.prototype.onAdd = function (vmlRoot) { append(vmlRoot, this._vmlEl); this.appendRectText(vmlRoot); }; /*************************************************** * TEXT **************************************************/ var DEFAULT_STYLE_NORMAL = 'normal'; var fontStyleCache = {}; var fontStyleCacheCount = 0; var MAX_FONT_CACHE_SIZE = 100; var fontEl = document.createElement('div'); var getFontStyle = function (fontString) { var fontStyle = fontStyleCache[fontString]; if (!fontStyle) { // Clear cache if (fontStyleCacheCount > MAX_FONT_CACHE_SIZE) { fontStyleCacheCount = 0; fontStyleCache = {}; } var style = fontEl.style; var fontFamily; try { style.font = fontString; fontFamily = style.fontFamily.split(',')[0]; } catch (e) { } fontStyle = { style: style.fontStyle || DEFAULT_STYLE_NORMAL, variant: style.fontVariant || DEFAULT_STYLE_NORMAL, weight: style.fontWeight || DEFAULT_STYLE_NORMAL, size: parseFloat(style.fontSize || 12) | 0, family: fontFamily || 'Microsoft YaHei' }; fontStyleCache[fontString] = fontStyle; fontStyleCacheCount++; } return fontStyle; }; var textMeasureEl; // Overwrite measure text method $override$1('measureText', function (text, textFont) { var doc$$1 = doc; if (!textMeasureEl) { textMeasureEl = doc$$1.createElement('div'); textMeasureEl.style.cssText = 'position:absolute;top:-20000px;left:0;' + 'padding:0;margin:0;border:none;white-space:pre;'; doc.body.appendChild(textMeasureEl); } try { textMeasureEl.style.font = textFont; } catch (ex) { // Ignore failures to set to invalid font. } textMeasureEl.innerHTML = ''; // Don't use innerHTML or innerText because they allow markup/whitespace. textMeasureEl.appendChild(doc$$1.createTextNode(text)); return { width: textMeasureEl.offsetWidth }; }); var tmpRect$2 = new BoundingRect(); var drawRectText = function (vmlRoot, rect, textRect, fromTextEl) { var style = this.style; // Optimize, avoid normalize every time. this.__dirty && normalizeTextStyle(style, true); var text = style.text; // Convert to string text != null && (text += ''); if (!text) { return; } // Convert rich text to plain text. Rich text is not supported in // IE8-, but tags in rich text template will be removed. if (style.rich) { var contentBlock = parseRichText(text, style); text = []; for (var i = 0; i < contentBlock.lines.length; i++) { var tokens = contentBlock.lines[i].tokens; var textLine = []; for (var j = 0; j < tokens.length; j++) { textLine.push(tokens[j].text); } text.push(textLine.join('')); } text = text.join('\n'); } var x; var y; var align = style.textAlign; var verticalAlign = style.textVerticalAlign; var fontStyle = getFontStyle(style.font); // FIXME encodeHtmlAttribute ? var font = fontStyle.style + ' ' + fontStyle.variant + ' ' + fontStyle.weight + ' ' + fontStyle.size + 'px "' + fontStyle.family + '"'; textRect = textRect || getBoundingRect( text, font, align, verticalAlign, style.textPadding, style.textLineHeight ); // Transform rect to view space var m = this.transform; // Ignore transform for text in other element if (m && !fromTextEl) { tmpRect$2.copy(rect); tmpRect$2.applyTransform(m); rect = tmpRect$2; } if (!fromTextEl) { var textPosition = style.textPosition; // Text position represented by coord if (textPosition instanceof Array) { x = rect.x + parsePercent$3(textPosition[0], rect.width); y = rect.y + parsePercent$3(textPosition[1], rect.height); align = align || 'left'; } else { var res = this.calculateTextPosition ? this.calculateTextPosition({}, style, rect) : calculateTextPosition({}, style, rect); x = res.x; y = res.y; // Default align and baseline when has textPosition align = align || res.textAlign; verticalAlign = verticalAlign || res.textVerticalAlign; } } else { x = rect.x; y = rect.y; } x = adjustTextX(x, textRect.width, align); y = adjustTextY(y, textRect.height, verticalAlign); // Force baseline 'middle' y += textRect.height / 2; // var fontSize = fontStyle.size; // 1.75 is an arbitrary number, as there is no info about the text baseline // switch (baseline) { // case 'hanging': // case 'top': // y += fontSize / 1.75; // break; // case 'middle': // break; // default: // // case null: // // case 'alphabetic': // // case 'ideographic': // // case 'bottom': // y -= fontSize / 2.25; // break; // } // switch (align) { // case 'left': // break; // case 'center': // x -= textRect.width / 2; // break; // case 'right': // x -= textRect.width; // break; // case 'end': // align = elementStyle.direction == 'ltr' ? 'right' : 'left'; // break; // case 'start': // align = elementStyle.direction == 'rtl' ? 'right' : 'left'; // break; // default: // align = 'left'; // } var createNode$$1 = createNode; var textVmlEl = this._textVmlEl; var pathEl; var textPathEl; var skewEl; if (!textVmlEl) { textVmlEl = createNode$$1('line'); pathEl = createNode$$1('path'); textPathEl = createNode$$1('textpath'); skewEl = createNode$$1('skew'); // FIXME Why here is not cammel case // Align 'center' seems wrong textPathEl.style['v-text-align'] = 'left'; initRootElStyle(textVmlEl); pathEl.textpathok = true; textPathEl.on = true; textVmlEl.from = '0 0'; textVmlEl.to = '1000 0.05'; append(textVmlEl, skewEl); append(textVmlEl, pathEl); append(textVmlEl, textPathEl); this._textVmlEl = textVmlEl; } else { // 这里是在前面 appendChild 保证顺序的前提下 skewEl = textVmlEl.firstChild; pathEl = skewEl.nextSibling; textPathEl = pathEl.nextSibling; } var coords = [x, y]; var textVmlElStyle = textVmlEl.style; // Ignore transform for text in other element if (m && fromTextEl) { applyTransform(coords, coords, m); skewEl.on = true; skewEl.matrix = m[0].toFixed(3) + comma + m[2].toFixed(3) + comma + m[1].toFixed(3) + comma + m[3].toFixed(3) + ',0,0'; // Text position skewEl.offset = (round$2(coords[0]) || 0) + ',' + (round$2(coords[1]) || 0); // Left top point as origin skewEl.origin = '0 0'; textVmlElStyle.left = '0px'; textVmlElStyle.top = '0px'; } else { skewEl.on = false; textVmlElStyle.left = round$2(x) + 'px'; textVmlElStyle.top = round$2(y) + 'px'; } textPathEl.string = encodeHtmlAttribute(text); // TODO try { textPathEl.style.font = font; } // Error font format catch (e) {} updateFillAndStroke(textVmlEl, 'fill', { fill: style.textFill, opacity: style.opacity }, this); updateFillAndStroke(textVmlEl, 'stroke', { stroke: style.textStroke, opacity: style.opacity, lineDash: style.lineDash || null // style.lineDash can be `false`. }, this); textVmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Attached to root append(vmlRoot, textVmlEl); }; var removeRectText = function (vmlRoot) { remove(vmlRoot, this._textVmlEl); this._textVmlEl = null; }; var appendRectText = function (vmlRoot) { append(vmlRoot, this._textVmlEl); }; var list = [RectText, Displayable, ZImage, Path, Text]; // In case Displayable has been mixed in RectText for (var i$1 = 0; i$1 < list.length; i$1++) { var proto$7 = list[i$1].prototype; proto$7.drawRectText = drawRectText; proto$7.removeRectText = removeRectText; proto$7.appendRectText = appendRectText; } Text.prototype.brushVML = function (vmlRoot) { var style = this.style; if (style.text != null) { this.drawRectText(vmlRoot, { x: style.x || 0, y: style.y || 0, width: 0, height: 0 }, this.getBoundingRect(), true); } else { this.removeRectText(vmlRoot); } }; Text.prototype.onRemove = function (vmlRoot) { this.removeRectText(vmlRoot); }; Text.prototype.onAdd = function (vmlRoot) { this.appendRectText(vmlRoot); }; } /** * VML Painter. * * @module zrender/vml/Painter */ function parseInt10$1(val) { return parseInt(val, 10); } /** * @alias module:zrender/vml/Painter */ function VMLPainter(root, storage) { initVML(); this.root = root; this.storage = storage; var vmlViewport = document.createElement('div'); var vmlRoot = document.createElement('div'); vmlViewport.style.cssText = 'display:inline-block;overflow:hidden;position:relative;width:300px;height:150px;'; vmlRoot.style.cssText = 'position:absolute;left:0;top:0;'; root.appendChild(vmlViewport); this._vmlRoot = vmlRoot; this._vmlViewport = vmlViewport; this.resize(); // Modify storage var oldDelFromStorage = storage.delFromStorage; var oldAddToStorage = storage.addToStorage; storage.delFromStorage = function (el) { oldDelFromStorage.call(storage, el); if (el) { el.onRemove && el.onRemove(vmlRoot); } }; storage.addToStorage = function (el) { // Displayable already has a vml node el.onAdd && el.onAdd(vmlRoot); oldAddToStorage.call(storage, el); }; this._firstPaint = true; } VMLPainter.prototype = { constructor: VMLPainter, getType: function () { return 'vml'; }, /** * @return {HTMLDivElement} */ getViewportRoot: function () { return this._vmlViewport; }, getViewportRootOffset: function () { var viewportRoot = this.getViewportRoot(); if (viewportRoot) { return { offsetLeft: viewportRoot.offsetLeft || 0, offsetTop: viewportRoot.offsetTop || 0 }; } }, /** * 刷新 */ refresh: function () { var list = this.storage.getDisplayList(true, true); this._paintList(list); }, _paintList: function (list) { var vmlRoot = this._vmlRoot; for (var i = 0; i < list.length; i++) { var el = list[i]; if (el.invisible || el.ignore) { if (!el.__alreadyNotVisible) { el.onRemove(vmlRoot); } // Set as already invisible el.__alreadyNotVisible = true; } else { if (el.__alreadyNotVisible) { el.onAdd(vmlRoot); } el.__alreadyNotVisible = false; if (el.__dirty) { el.beforeBrush && el.beforeBrush(); (el.brushVML || el.brush).call(el, vmlRoot); el.afterBrush && el.afterBrush(); } } el.__dirty = false; } if (this._firstPaint) { // Detached from document at first time // to avoid page refreshing too many times // FIXME 如果每次都先 removeChild 可能会导致一些填充和描边的效果改变 this._vmlViewport.appendChild(vmlRoot); this._firstPaint = false; } }, resize: function (width, height) { var width = width == null ? this._getWidth() : width; var height = height == null ? this._getHeight() : height; if (this._width !== width || this._height !== height) { this._width = width; this._height = height; var vmlViewportStyle = this._vmlViewport.style; vmlViewportStyle.width = width + 'px'; vmlViewportStyle.height = height + 'px'; } }, dispose: function () { this.root.innerHTML = ''; this._vmlRoot = this._vmlViewport = this.storage = null; }, getWidth: function () { return this._width; }, getHeight: function () { return this._height; }, clear: function () { if (this._vmlViewport) { this.root.removeChild(this._vmlViewport); } }, _getWidth: function () { var root = this.root; var stl = root.currentStyle; return ((root.clientWidth || parseInt10$1(stl.width)) - parseInt10$1(stl.paddingLeft) - parseInt10$1(stl.paddingRight)) | 0; }, _getHeight: function () { var root = this.root; var stl = root.currentStyle; return ((root.clientHeight || parseInt10$1(stl.height)) - parseInt10$1(stl.paddingTop) - parseInt10$1(stl.paddingBottom)) | 0; } }; // Not supported methods function createMethodNotSupport(method) { return function () { logError$1('In IE8.0 VML mode painter not support method "' + method + '"'); }; } // Unsupported methods each$1([ 'getLayer', 'insertLayer', 'eachLayer', 'eachBuiltinLayer', 'eachOtherLayer', 'getLayers', 'modLayer', 'delLayer', 'clearLayer', 'toDataURL', 'pathToImage' ], function (name) { VMLPainter.prototype[name] = createMethodNotSupport(name); }); registerPainter('vml', VMLPainter); var svgURI = 'http://www.w3.org/2000/svg'; function createElement(name) { return document.createElementNS(svgURI, name); } // TODO // 1. shadow // 2. Image: sx, sy, sw, sh var CMD$4 = PathProxy.CMD; var arrayJoin = Array.prototype.join; var NONE = 'none'; var mathRound = Math.round; var mathSin$3 = Math.sin; var mathCos$3 = Math.cos; var PI$3 = Math.PI; var PI2$5 = Math.PI * 2; var degree = 180 / PI$3; var EPSILON$4 = 1e-4; function round4(val) { return mathRound(val * 1e4) / 1e4; } function isAroundZero$1(val) { return val < EPSILON$4 && val > -EPSILON$4; } function pathHasFill(style, isText) { var fill = isText ? style.textFill : style.fill; return fill != null && fill !== NONE; } function pathHasStroke(style, isText) { var stroke = isText ? style.textStroke : style.stroke; return stroke != null && stroke !== NONE; } function setTransform(svgEl, m) { if (m) { attr(svgEl, 'transform', 'matrix(' + arrayJoin.call(m, ',') + ')'); } } function attr(el, key, val) { if (!val || val.type !== 'linear' && val.type !== 'radial') { // Don't set attribute for gradient, since it need new dom nodes el.setAttribute(key, val); } } function attrXLink(el, key, val) { el.setAttributeNS('http://www.w3.org/1999/xlink', key, val); } function bindStyle(svgEl, style, isText, el) { if (pathHasFill(style, isText)) { var fill = isText ? style.textFill : style.fill; fill = fill === 'transparent' ? NONE : fill; attr(svgEl, 'fill', fill); attr(svgEl, 'fill-opacity', style.fillOpacity != null ? style.fillOpacity * style.opacity : style.opacity); } else { attr(svgEl, 'fill', NONE); } if (pathHasStroke(style, isText)) { var stroke = isText ? style.textStroke : style.stroke; stroke = stroke === 'transparent' ? NONE : stroke; attr(svgEl, 'stroke', stroke); var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth; var strokeScale = !isText && style.strokeNoScale ? el.getLineScale() : 1; attr(svgEl, 'stroke-width', strokeWidth / strokeScale); // stroke then fill for text; fill then stroke for others attr(svgEl, 'paint-order', isText ? 'stroke' : 'fill'); attr(svgEl, 'stroke-opacity', style.strokeOpacity != null ? style.strokeOpacity : style.opacity); var lineDash = style.lineDash; if (lineDash) { attr(svgEl, 'stroke-dasharray', style.lineDash.join(',')); attr(svgEl, 'stroke-dashoffset', mathRound(style.lineDashOffset || 0)); } else { attr(svgEl, 'stroke-dasharray', ''); } // PENDING style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap); style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin); style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit); } else { attr(svgEl, 'stroke', NONE); } } /*************************************************** * PATH **************************************************/ function pathDataToString$1(path) { var str = []; var data = path.data; var dataLength = path.len(); for (var i = 0; i < dataLength;) { var cmd = data[i++]; var cmdStr = ''; var nData = 0; switch (cmd) { case CMD$4.M: cmdStr = 'M'; nData = 2; break; case CMD$4.L: cmdStr = 'L'; nData = 2; break; case CMD$4.Q: cmdStr = 'Q'; nData = 4; break; case CMD$4.C: cmdStr = 'C'; nData = 6; break; case CMD$4.A: var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var theta = data[i++]; var dTheta = data[i++]; var psi = data[i++]; var clockwise = data[i++]; var dThetaPositive = Math.abs(dTheta); var isCircle = isAroundZero$1(dThetaPositive - PI2$5) || (clockwise ? dTheta >= PI2$5 : -dTheta >= PI2$5); // Mapping to 0~2PI var unifiedTheta = dTheta > 0 ? dTheta % PI2$5 : (dTheta % PI2$5 + PI2$5); var large = false; if (isCircle) { large = true; } else if (isAroundZero$1(dThetaPositive)) { large = false; } else { large = (unifiedTheta >= PI$3) === !!clockwise; } var x0 = round4(cx + rx * mathCos$3(theta)); var y0 = round4(cy + ry * mathSin$3(theta)); // It will not draw if start point and end point are exactly the same // We need to shift the end point with a small value // FIXME A better way to draw circle ? if (isCircle) { if (clockwise) { dTheta = PI2$5 - 1e-4; } else { dTheta = -PI2$5 + 1e-4; } large = true; if (i === 9) { // Move to (x0, y0) only when CMD.A comes at the // first position of a shape. // For instance, when drawing a ring, CMD.A comes // after CMD.M, so it's unnecessary to move to // (x0, y0). str.push('M', x0, y0); } } var x = round4(cx + rx * mathCos$3(theta + dTheta)); var y = round4(cy + ry * mathSin$3(theta + dTheta)); // FIXME Ellipse str.push('A', round4(rx), round4(ry), mathRound(psi * degree), +large, +clockwise, x, y); break; case CMD$4.Z: cmdStr = 'Z'; break; case CMD$4.R: var x = round4(data[i++]); var y = round4(data[i++]); var w = round4(data[i++]); var h = round4(data[i++]); str.push( 'M', x, y, 'L', x + w, y, 'L', x + w, y + h, 'L', x, y + h, 'L', x, y ); break; } cmdStr && str.push(cmdStr); for (var j = 0; j < nData; j++) { // PENDING With scale str.push(round4(data[i++])); } } return str.join(' '); } var svgPath = {}; svgPath.brush = function (el) { var style = el.style; var svgEl = el.__svgEl; if (!svgEl) { svgEl = createElement('path'); el.__svgEl = svgEl; } if (!el.path) { el.createPathProxy(); } var path = el.path; if (el.__dirtyPath) { path.beginPath(); path.subPixelOptimize = false; el.buildPath(path, el.shape); el.__dirtyPath = false; var pathStr = pathDataToString$1(path); if (pathStr.indexOf('NaN') < 0) { // Ignore illegal path, which may happen such in out-of-range // data in Calendar series. attr(svgEl, 'd', pathStr); } } bindStyle(svgEl, style, false, el); setTransform(svgEl, el.transform); if (style.text != null) { svgTextDrawRectText(el, el.getBoundingRect()); } else { removeOldTextNode(el); } }; /*************************************************** * IMAGE **************************************************/ var svgImage = {}; svgImage.brush = function (el) { var style = el.style; var image = style.image; if (image instanceof HTMLImageElement) { var src = image.src; image = src; } if (!image) { return; } var x = style.x || 0; var y = style.y || 0; var dw = style.width; var dh = style.height; var svgEl = el.__svgEl; if (!svgEl) { svgEl = createElement('image'); el.__svgEl = svgEl; } if (image !== el.__imageSrc) { attrXLink(svgEl, 'href', image); // Caching image src el.__imageSrc = image; } attr(svgEl, 'width', dw); attr(svgEl, 'height', dh); attr(svgEl, 'x', x); attr(svgEl, 'y', y); setTransform(svgEl, el.transform); if (style.text != null) { svgTextDrawRectText(el, el.getBoundingRect()); } else { removeOldTextNode(el); } }; /*************************************************** * TEXT **************************************************/ var svgText = {}; var _tmpTextHostRect = new BoundingRect(); var _tmpTextBoxPos = {}; var _tmpTextTransform = []; var TEXT_ALIGN_TO_ANCHRO = { left: 'start', right: 'end', center: 'middle', middle: 'middle' }; /** * @param {module:zrender/Element} el * @param {Object|boolean} [hostRect] {x, y, width, height} * If set false, rect text is not used. */ var svgTextDrawRectText = function (el, hostRect) { var style = el.style; var elTransform = el.transform; var needTransformTextByHostEl = el instanceof Text || style.transformText; el.__dirty && normalizeTextStyle(style, true); var text = style.text; // Convert to string text != null && (text += ''); if (!needDrawText(text, style)) { return; } // render empty text for svg if no text but need draw text. text == null && (text = ''); // Follow the setting in the canvas renderer, if not transform the // text, transform the hostRect, by which the text is located. if (!needTransformTextByHostEl && elTransform) { _tmpTextHostRect.copy(hostRect); _tmpTextHostRect.applyTransform(elTransform); hostRect = _tmpTextHostRect; } var textSvgEl = el.__textSvgEl; if (!textSvgEl) { textSvgEl = createElement('text'); el.__textSvgEl = textSvgEl; } // style.font has been normalized by `normalizeTextStyle`. var textSvgElStyle = textSvgEl.style; var font = style.font || DEFAULT_FONT$1; var computedFont = textSvgEl.__computedFont; if (font !== textSvgEl.__styleFont) { textSvgElStyle.font = textSvgEl.__styleFont = font; // The computedFont might not be the orginal font if it is illegal font. computedFont = textSvgEl.__computedFont = textSvgElStyle.font; } var textPadding = style.textPadding; var textLineHeight = style.textLineHeight; var contentBlock = el.__textCotentBlock; if (!contentBlock || el.__dirtyText) { contentBlock = el.__textCotentBlock = parsePlainText( text, computedFont, textPadding, textLineHeight, style.truncate ); } var outerHeight = contentBlock.outerHeight; var lineHeight = contentBlock.lineHeight; getBoxPosition(_tmpTextBoxPos, el, style, hostRect); var baseX = _tmpTextBoxPos.baseX; var baseY = _tmpTextBoxPos.baseY; var textAlign = _tmpTextBoxPos.textAlign || 'left'; var textVerticalAlign = _tmpTextBoxPos.textVerticalAlign; setTextTransform( textSvgEl, needTransformTextByHostEl, elTransform, style, hostRect, baseX, baseY ); var boxY = adjustTextY(baseY, outerHeight, textVerticalAlign); var textX = baseX; var textY = boxY; // TODO needDrawBg if (textPadding) { textX = getTextXForPadding$1(baseX, textAlign, textPadding); textY += textPadding[0]; } // `textBaseline` is set as 'middle'. textY += lineHeight / 2; bindStyle(textSvgEl, style, true, el); // FIXME // Add a