You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
43087 lines
1.2 MiB
43087 lines
1.2 MiB
(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). |
|
|
|
/** |
|
* 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. |
|
*/ |
|
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, |
|
// <http://caniuse.com/#search=pointer%20event>. |
|
pointerEventsSupported: 'onpointerdown' in window // 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. |
|
// 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) |
|
&& (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) { |
|
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) { |
|
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.<number>} val |
|
* @return {Array.<number>} |
|
*/ |
|
|
|
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)); |
|
|
|
for (var key in this.data) { |
|
this.data.hasOwnProperty(key) && cb(this.data[key], key); |
|
} |
|
}, |
|
// 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 |
|
}); |
|
|
|
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); |
|
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; |
|
|
|
if (draggingTarget && draggingTarget.draggable) { |
|
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] Call 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. |
|
* @param {Function} [handler] The event handler. |
|
*/ |
|
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; |
|
} // ---------------------- |
|
|
|
/** |
|
* 事件辅助类 |
|
* @module zrender/core/event |
|
* @author Kener (@Kener-林峰, kener.linfeng@gmail.com) |
|
*/ |
|
var isDomLevel2 = typeof window !== 'undefined' && !!window.addEventListener; |
|
var MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; |
|
|
|
function getBoundingClientRect(el) { |
|
// BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect |
|
return el.getBoundingClientRect ? el.getBoundingClientRect() : { |
|
left: 0, |
|
top: 0 |
|
}; |
|
} // `calculate` is optional, default false |
|
|
|
|
|
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. |
|
// FIXME |
|
// When mousemove event triggered on ec tooltip, target is not zr painter.dom, and |
|
// offsetX/Y is relative to e.target, where the calculation of zrX/Y via offsetX/Y |
|
// is too complex. So css-transfrom dont support in this case temporarily. |
|
|
|
if (calculate || !env$1.canvasSupported) { |
|
defaultGetZrXY(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. |
|
// <https://bugs.jquery.com/ticket/8523#comment:14> |
|
// 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 { |
|
defaultGetZrXY(el, e, out); |
|
} |
|
|
|
return out; |
|
} |
|
|
|
function defaultGetZrXY(el, e, out) { |
|
// This well-known method below does not support css transform. |
|
var box = getBoundingClientRect(el); |
|
out.zrX = e.clientX - box.left; |
|
out.zrY = e.clientY - box.top; |
|
} |
|
/** |
|
* 如果存在第三方嵌入的一些dom触发的事件,或touch事件,需要转换一下事件坐标. |
|
* `calculate` is optional, default false. |
|
*/ |
|
|
|
|
|
function normalizeEvent(el, e, calculate) { |
|
e = e || window.event; |
|
|
|
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, if 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 |
|
*/ |
|
|
|
function addEventListener(el, name, handler) { |
|
if (isDomLevel2) { |
|
// Reproduct the console warning: |
|
// [Violation] Added non-passive event listener to a scroll-blocking <some> 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); |
|
} else { |
|
el.attachEvent('on' + name, handler); |
|
} |
|
} |
|
function removeEventListener(el, name, handler) { |
|
if (isDomLevel2) { |
|
el.removeEventListener(name, handler); |
|
} else { |
|
el.detachEvent('on' + name, handler); |
|
} |
|
} |
|
/** |
|
* preventDefault and stopPropagation. |
|
* Notice: do not do that in zrender. Upper application |
|
* do that if necessary. |
|
* |
|
* @memberOf module:zrender/core/event |
|
* @method |
|
* @param {Event} e : 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} |
|
*/ |
|
|
|
|
|
/** |
|
* To be removed. |
|
* @deprecated |
|
*/ |
|
|
|
// 做向上兼容 |
|
|
|
/** |
|
* Only implements needed gestures for mobile. |
|
*/ |
|
var GestureMgr = function () { |
|
/** |
|
* @private |
|
* @type {Array.<Object>} |
|
*/ |
|
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. |
|
|
|
}; |
|
|
|
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(event) { |
|
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 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 = 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) { |
|
this.dispatchToElement(this._hovered, 'mouseout', event); // 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; |
|
var innerDom; |
|
|
|
do { |
|
element = element && element.parentNode; |
|
} while (element && element.nodeType !== 9 && !(innerDom = element === this.painterRoot)); |
|
|
|
!innerDom && this.trigger('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) { |
|
// Find hover again to avoid click event is dispatched manually. Or click is triggered without mouseover |
|
var hovered = this.findHover(event.zrX, event.zrY); |
|
var 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; |
|
} |
|
|
|
mixin(Handler, Eventful); |
|
mixin(Handler, Draggable); |
|
|
|
/** |
|
* 3x2矩阵操作类 |
|
* @exports zrender/tool/matrix |
|
*/ |
|
var ArrayCtor$1 = typeof Float32Array === 'undefined' ? Array : Float32Array; |
|
/** |
|
* Create a identity matrix. |
|
* @return {Float32Array|Array.<number>} |
|
*/ |
|
|
|
function create$1() { |
|
var out = new ArrayCtor$1(6); |
|
identity(out); |
|
return out; |
|
} |
|
/** |
|
* 设置矩阵为单位矩阵 |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} m1 |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} a |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} a |
|
* @param {Float32Array|Array.<number>} 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.<number>} out |
|
* @param {Float32Array|Array.<number>} 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.<number>} 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.<number>} |
|
* @default [0, 0] |
|
*/ |
|
this.position = [0, 0]; |
|
} |
|
|
|
if (opts.rotation == null) { |
|
/** |
|
* 旋转 |
|
* @type {Array.<number>} |
|
* @default 0 |
|
*/ |
|
this.rotation = 0; |
|
} |
|
|
|
if (!opts.scale) { |
|
/** |
|
* 缩放 |
|
* @type {Array.<number>} |
|
* @default [1, 1] |
|
*/ |
|
this.scale = [1, 1]; |
|
} |
|
/** |
|
* 旋转和缩放的原点 |
|
* @type {Array.<number>} |
|
* @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.<number>} |
|
*/ |
|
|
|
|
|
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.<number>} |
|
*/ |
|
|
|
|
|
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.<number>} |
|
*/ |
|
|
|
|
|
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.<number>} target.origin |
|
* @param {number} target.rotation |
|
* @param {Array.<number>} target.position |
|
* @param {Array.<number>} [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.<number>} out |
|
* @return {Array.<number>} |
|
* @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.<number>} hsla |
|
* @param {Array.<number>} rgba |
|
* @return {Array.<number>} 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.<number>} rgba |
|
* @return {Array.<number>} 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.<Array.<number>>} colors List of rgba color array |
|
* @param {Array.<number>} [out] Mapped gba color array |
|
* @return {Array.<number>} 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.<string>} 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.<number>} 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 = { |
|
/** |
|
* 设置动画关键帧 |
|
* @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); |
|
} |
|
}, |
|
|
|
/** |
|
* 开始执行动画 |
|
* @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; |
|
}, |
|
|
|
/** |
|
* 停止动画 |
|
* @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; |
|
}, |
|
|
|
/** |
|
* 设置动画延迟开始的时间 |
|
* @param {number} time 单位ms |
|
* @return {module:zrender/animation/Animator} |
|
*/ |
|
delay: function (time) { |
|
this._delay = time; |
|
return this; |
|
}, |
|
|
|
/** |
|
* 添加动画结束的回调 |
|
* @param {Function} cb |
|
* @return {module:zrender/animation/Animator} |
|
*/ |
|
done: function (cb) { |
|
if (cb) { |
|
this._doneList.push(cb); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* @return {Array.<module:zrender/animation/Clip>} |
|
*/ |
|
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日志选项:catchBrushException为true下有效 |
|
* 0 : 不生成debug数据,发布用 |
|
* 1 : 异常抛出,调试用 |
|
* 2 : 控制台输出,调试用 |
|
*/ |
|
|
|
|
|
var debugMode = 0; // retina 屏幕优化 |
|
|
|
var devicePixelRatio = dpr; |
|
|
|
var log = function () {}; |
|
|
|
if (debugMode === 1) { |
|
log = function () { |
|
for (var k in arguments) { |
|
throw new Error(arguments[k]); |
|
} |
|
}; |
|
} else if (debugMode > 1) { |
|
log = function () { |
|
for (var k in arguments) { |
|
console.log(arguments[k]); |
|
} |
|
}; |
|
} |
|
|
|
var log$1 = log; |
|
|
|
/** |
|
* @alias modue:zrender/mixin/Animatable |
|
* @constructor |
|
*/ |
|
|
|
var Animatable = function () { |
|
/** |
|
* @type {Array.<module:zrender/animation/Animator>} |
|
* @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) { |
|
log$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.<number>} 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.<number>} |
|
*/ |
|
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.<module:zrender/Element>} |
|
*/ |
|
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, count2, 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(); |
|
} |
|
|
|
// 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.<module:zrender/graphic/Displayable>} |
|
*/ |
|
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.<string>} [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, |
|
|
|
/** |
|
* @type {Array.<number>} |
|
*/ |
|
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.<number>} |
|
* @default 'inside' |
|
*/ |
|
textPosition: 'inside', |
|
|
|
/** |
|
* If not specified, use the boundingRect of a `displayable`. |
|
* @type {Object} |
|
*/ |
|
textRect: null, |
|
|
|
/** |
|
* [x, y] |
|
* @type {Array.<number>} |
|
*/ |
|
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 useful in Path and Image element |
|
* @type {boolean} |
|
*/ |
|
transformText: false, |
|
|
|
/** |
|
* Text rotate around position of Path or Image |
|
* Only useful in Path and Image element and transformText is false. |
|
*/ |
|
textRotation: 0, |
|
|
|
/** |
|
* Text origin of text rotation, like [10, 40]. |
|
* Based on x, y of rect. |
|
* Useful in label rotation of circular symbol. |
|
* By default, this origin is textPosition. |
|
* Can be 'center'. |
|
* @type {string|Array.<number>} |
|
*/ |
|
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.<number>} |
|
*/ |
|
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; |
|
domStyle['margin'] = 0; |
|
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 = {}; |
|
|
|
/** |
|
* @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.<number>} [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; |
|
} |
|
/** |
|
* @public |
|
* @param {stirng} textPosition |
|
* @param {Object} rect {x, y, width, height} |
|
* @param {number} distance |
|
* @return {Object} {x, y, textAlign, textVerticalAlign} |
|
*/ |
|
|
|
function adjustTextPositionOnRect(textPosition, rect, distance) { |
|
var x = rect.x; |
|
var y = rect.y; |
|
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; |
|
} |
|
|
|
return { |
|
x: x, |
|
y: y, |
|
textAlign: textAlign, |
|
textVerticalAlign: 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} |
|
* Notice: for performance, do not calculate outerWidth util needed. |
|
*/ |
|
|
|
|
|
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; |
|
|
|
if (padding) { |
|
outerHeight += padding[0] + padding[2]; |
|
} |
|
|
|
if (text && truncate) { |
|
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 |
|
}; |
|
} |
|
/** |
|
* 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']]; |
|
/** |
|
* @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(outerHeight, 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(outerHeight, 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(blockHeiht, 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 = adjustTextPositionOnRect(textPosition, rect, style.textDistance); |
|
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]; |
|
} |
|
} |
|
|
|
return { |
|
baseX: baseX, |
|
baseY: baseY, |
|
textAlign: textAlign, |
|
textVerticalAlign: textVerticalAlign |
|
}; |
|
} |
|
|
|
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. |
|
|
|
this.__clipPaths = []; // FIXME Stateful must be mixined after style is setted |
|
// Stateful.call(this, opts); |
|
} |
|
|
|
Displayable.prototype = { |
|
constructor: Displayable, |
|
type: 'displayable', |
|
|
|
/** |
|
* Displayable 是否为脏,Painter 中会根据该标记判断是否需要是否需要重新绘制 |
|
* Dirty flag. From which painter will determine if this displayable object needs brush |
|
* @name module:zrender/graphic/Displayable#__dirty |
|
* @type {boolean} |
|
*/ |
|
__dirty: true, |
|
|
|
/** |
|
* 图形是否可见,为true时不绘制图形,但是仍能触发鼠标事件 |
|
* If ignore drawing of the displayable object. Mouse event will still be triggered |
|
* @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, |
|
|
|
/** |
|
* z层level,决定绘画在哪层canvas中 |
|
* @name module:/zrender/graphic/Displayable#zlevel |
|
* @type {number} |
|
* @default 0 |
|
*/ |
|
zlevel: 0, |
|
|
|
/** |
|
* 是否可拖拽 |
|
* @name module:/zrender/graphic/Displayable#draggable |
|
* @type {boolean} |
|
* @default false |
|
*/ |
|
draggable: false, |
|
|
|
/** |
|
* 是否正在拖拽 |
|
* @name module:/zrender/graphic/Displayable#draggable |
|
* @type {boolean} |
|
* @default false |
|
*/ |
|
dragging: false, |
|
|
|
/** |
|
* 是否相应鼠标事件 |
|
* @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) {}, |
|
|
|
/** |
|
* 图形绘制方法 |
|
* @param {CanvasRenderingContext2D} ctx |
|
*/ |
|
// Interface |
|
brush: function (ctx, prevEl) {}, |
|
|
|
/** |
|
* 获取最小包围盒 |
|
* @return {module:zrender/core/BoundingRect} |
|
*/ |
|
// Interface |
|
getBoundingRect: function () {}, |
|
|
|
/** |
|
* 判断坐标 x, y 是否在图形上 |
|
* 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); |
|
}, |
|
|
|
/** |
|
* 判断坐标 x, y 是否在图形的包围盒上 |
|
* 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, 通过 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; |
|
} |
|
}; |
|
inherits(Displayable, Element); |
|
mixin(Displayable, RectText); // zrUtil.mixin(Displayable, Stateful); |
|
|
|
/** |
|
* @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) { |
|
if (clipPaths === prevClipPaths) { |
|
// Can both be null or undefined |
|
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; |
|
} |
|
} |
|
} |
|
|
|
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; // 避免页面选中的尴尬 |
|
|
|
domRoot.style.cssText = ['position:relative', '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.<number>} |
|
* @private |
|
*/ |
|
|
|
var zlevelList = this._zlevelList = []; |
|
/** |
|
* @type {Object.<string, module:zrender/Layer>} |
|
* @private |
|
*/ |
|
|
|
var layers = this._layers = {}; |
|
/** |
|
* @type {Object.<string, 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; // Optimize when clipping on group with several elements |
|
|
|
if (!scope.prevElClipPaths || isClipPathChanged(clipPaths, scope.prevElClipPaths)) { |
|
// If has previous clipping state, restore from it |
|
if (scope.prevElClipPaths) { |
|
currentLayer.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); |
|
} |
|
|
|
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]) { |
|
log$1('ZLevel ' + zlevel + ' has been used already'); |
|
return; |
|
} // Check if is a valid layer |
|
|
|
|
|
if (!isLayerValid(layer)) { |
|
log$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.<module:zrender/Layer>} [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; |
|
|
|
for (var i = 0; i < list.length; i++) { |
|
var el = list[i]; |
|
var zlevel = el.zlevel; |
|
var layer; // PENDING If change one incremental element style ? |
|
// TODO Where there are non-incremental elements between incremental elements. |
|
|
|
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__) { |
|
log$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]; |
|
|
|
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; |
|
} |
|
}; |
|
|
|
/** |
|
* 动画主类, 调度和管理所有动画控制器 |
|
* |
|
* @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, |
|
|
|
/** |
|
* 添加 clip |
|
* @param {module:zrender/animation/Clip} clip |
|
*/ |
|
addClip: function (clip) { |
|
this._clips.push(clip); |
|
}, |
|
|
|
/** |
|
* 添加 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]); |
|
} |
|
}, |
|
|
|
/** |
|
* 删除动画片段 |
|
* @param {module:zrender/animation/Clip} clip |
|
*/ |
|
removeClip: function (clip) { |
|
var idx = indexOf(this._clips, clip); |
|
|
|
if (idx >= 0) { |
|
this._clips.splice(idx, 1); |
|
} |
|
}, |
|
|
|
/** |
|
* 删除动画片段 |
|
* @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); |
|
|
|
var TOUCH_CLICK_DELAY = 300; |
|
var mouseHandlerNames = ['click', 'dblclick', 'mousewheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu']; |
|
var touchHandlerNames = ['touchstart', 'touchend', 'touchmove']; |
|
var pointerEventNames = { |
|
pointerdown: 1, |
|
pointerup: 1, |
|
pointermove: 1, |
|
pointerout: 1 |
|
}; |
|
var pointerHandlerNames = map(mouseHandlerNames, function (name) { |
|
var nm = name.replace('mouse', 'pointer'); |
|
return pointerEventNames[nm] ? nm : name; |
|
}); |
|
|
|
function eventNameFix(name) { |
|
return name === 'mousewheel' && env$1.browser.firefox ? 'DOMMouseScroll' : name; |
|
} // 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 <https://github.com/deltakosh/handjs/blob/master/src/hand.base.js> |
|
* 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. |
|
*/ |
|
|
|
|
|
function setTouchTimer(instance) { |
|
instance._touching = true; |
|
clearTimeout(instance._touchTimer); |
|
instance._touchTimer = setTimeout(function () { |
|
instance._touching = false; |
|
}, 700); |
|
} |
|
|
|
var domHandlers = { |
|
/** |
|
* Mouse move handler |
|
* @inner |
|
* @param {Event} event |
|
*/ |
|
mousemove: function (event) { |
|
event = normalizeEvent(this.dom, event); |
|
this.trigger('mousemove', event); |
|
}, |
|
|
|
/** |
|
* Mouse out handler |
|
* @inner |
|
* @param {Event} event |
|
*/ |
|
mouseout: function (event) { |
|
event = normalizeEvent(this.dom, event); |
|
var element = event.toElement || event.relatedTarget; |
|
|
|
if (element !== this.dom) { |
|
while (element && element.nodeType !== 9) { |
|
// 忽略包含在root中的dom引起的mouseOut |
|
if (element === this.dom) { |
|
return; |
|
} |
|
|
|
element = element.parentNode; |
|
} |
|
} |
|
|
|
this.trigger('mouseout', event); |
|
}, |
|
|
|
/** |
|
* Touch开始响应函数 |
|
* @inner |
|
* @param {Event} 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); // Mark touch, which is useful in distinguish touch and |
|
// mouse event in upper applicatoin. |
|
|
|
event.zrByTouch = true; |
|
this._lastTouchMoment = new Date(); |
|
this.handler.processGesture(this, event, 'start'); // In touch device, trigger `mousemove`(`mouseover`) should |
|
// be triggered, and must before `mousedown` triggered. |
|
|
|
domHandlers.mousemove.call(this, event); |
|
domHandlers.mousedown.call(this, event); |
|
setTouchTimer(this); |
|
}, |
|
|
|
/** |
|
* Touch移动响应函数 |
|
* @inner |
|
* @param {Event} event |
|
*/ |
|
touchmove: function (event) { |
|
event = normalizeEvent(this.dom, event); // Mark touch, which is useful in distinguish touch and |
|
// mouse event in upper applicatoin. |
|
|
|
event.zrByTouch = true; |
|
this.handler.processGesture(this, 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. |
|
|
|
domHandlers.mousemove.call(this, event); |
|
setTouchTimer(this); |
|
}, |
|
|
|
/** |
|
* Touch结束响应函数 |
|
* @inner |
|
* @param {Event} event |
|
*/ |
|
touchend: function (event) { |
|
event = normalizeEvent(this.dom, event); // Mark touch, which is useful in distinguish touch and |
|
// mouse event in upper applicatoin. |
|
|
|
event.zrByTouch = true; |
|
this.handler.processGesture(this, event, 'end'); |
|
domHandlers.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) { |
|
domHandlers.click.call(this, event); |
|
} |
|
|
|
setTouchTimer(this); |
|
}, |
|
pointerdown: function (event) { |
|
domHandlers.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)) { |
|
domHandlers.mousemove.call(this, event); |
|
} |
|
}, |
|
pointerup: function (event) { |
|
domHandlers.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 domHandlers.touchend for detailed explanation) |
|
if (!isPointerFromTouch(event)) { |
|
domHandlers.mouseout.call(this, event); |
|
} |
|
} |
|
}; |
|
|
|
function isPointerFromTouch(event) { |
|
var pointerType = event.pointerType; |
|
return pointerType === 'pen' || pointerType === 'touch'; |
|
} // function useMSGuesture(handlerProxy, event) { |
|
// return isPointerFromTouch(event) && !!handlerProxy._msGesture; |
|
// } |
|
// Common handlers |
|
|
|
|
|
each$1(['click', 'mousedown', 'mouseup', 'mousewheel', 'dblclick', 'contextmenu'], function (name) { |
|
domHandlers[name] = function (event) { |
|
event = normalizeEvent(this.dom, event); |
|
this.trigger(name, event); |
|
}; |
|
}); |
|
/** |
|
* 为控制类实例初始化dom 事件处理函数 |
|
* |
|
* @inner |
|
* @param {module:zrender/Handler} instance 控制类实例 |
|
*/ |
|
|
|
function initDomHandler(instance) { |
|
each$1(touchHandlerNames, function (name) { |
|
instance._handlers[name] = bind(domHandlers[name], instance); |
|
}); |
|
each$1(pointerHandlerNames, function (name) { |
|
instance._handlers[name] = bind(domHandlers[name], instance); |
|
}); |
|
each$1(mouseHandlerNames, function (name) { |
|
instance._handlers[name] = makeMouseHandler(domHandlers[name], instance); |
|
}); |
|
|
|
function makeMouseHandler(fn, instance) { |
|
return function () { |
|
if (instance._touching) { |
|
return; |
|
} |
|
|
|
return fn.apply(instance, arguments); |
|
}; |
|
} |
|
} |
|
|
|
function HandlerDomProxy(dom) { |
|
Eventful.call(this); |
|
this.dom = dom; |
|
/** |
|
* @private |
|
* @type {boolean} |
|
*/ |
|
|
|
this._touching = false; |
|
/** |
|
* @private |
|
* @type {number} |
|
*/ |
|
|
|
this._touchTimer; |
|
this._handlers = {}; |
|
initDomHandler(this); |
|
|
|
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. |
|
mountHandlers(pointerHandlerNames, this); // 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 <https://msdn.microsoft.com/en-us/library/dn433243(v=vs.85).aspx> |
|
// if (typeof MSGesture === 'function') { |
|
// (this._msGesture = new MSGesture()).target = dom; // jshint ignore:line |
|
// dom.addEventListener('MSGestureChange', onMSGestureChange); |
|
// } |
|
} else { |
|
if (env$1.touchEventsSupported) { |
|
mountHandlers(touchHandlerNames, this); // 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`. |
|
|
|
|
|
mountHandlers(mouseHandlerNames, this); |
|
} |
|
|
|
function mountHandlers(handlerNames, instance) { |
|
each$1(handlerNames, function (name) { |
|
addEventListener(dom, eventNameFix(name), instance._handlers[name]); |
|
}, instance); |
|
} |
|
} |
|
|
|
var handlerDomProxyProto = HandlerDomProxy.prototype; |
|
|
|
handlerDomProxyProto.dispose = function () { |
|
var handlerNames = mouseHandlerNames.concat(touchHandlerNames); |
|
|
|
for (var i = 0; i < handlerNames.length; i++) { |
|
var name = handlerNames[i]; |
|
removeEventListener(this.dom, eventNameFix(name), this._handlers[name]); |
|
} |
|
}; |
|
|
|
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.0.7'; |
|
/** |
|
* 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()) : 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 = false; |
|
this.painter.refresh(); |
|
/** |
|
* Avoid trigger zr.refresh in Element#beforeUpdate hook |
|
*/ |
|
|
|
this._needsRefresh = false; // var end = new Date(); |
|
// var log = document.getElementById('log'); |
|
// if (log) { |
|
// log.innerHTML = log.innerHTML + '<br>' + (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.<string>} 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.<number|string|Date>} |
|
*/ |
|
|
|
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.<Object>|Array.<module:echarts/model/Component>} exists |
|
* @param {Object|Array.<Object>} newCptOptions |
|
* @return {Array.<Object>} 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.<Object>} Result, like [{exist: ..., option: ...}, {}], |
|
* which order is the same as exists. |
|
* @return {Array.<Object>} 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.<Object>} batchA Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] |
|
* @param {Array.<Object>} batchB Like: [{seriesId: 2, dataIndex: [32, 4, 5]}, ...] |
|
* @return {Array.<Array.<Object>, Array.<Object>>} 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.<number>} 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.<number>|Array.<string> |
|
* 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.<string>} [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) { |
|
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; |
|
|
|
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.<subKey, componentClass>, 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) { |
|
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.<string>} 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.<string>} 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); |
|
var lineDash = this.getLineDash(style.lineWidth); |
|
lineDash && (style.lineDash = lineDash); |
|
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 ? null : 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.<number>} 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.<number>} 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.<number>} 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.<number>} [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.<number>} 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.<number>} 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.<number>} 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<Object>} 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.<number>} min |
|
* @param {Array.<number>} 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.<number>} min |
|
* @param {Array.<number>} 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.<number>} min |
|
* @param {Array.<number>} 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.<number>} min |
|
* @param {Array.<number>} 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 |
|
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.<Object>} |
|
*/ |
|
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) { |
|
this._ux = mathAbs(1 / devicePixelRatio / sx) || 0; |
|
this._uy = mathAbs(1 / 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.<module:zrender/core/PathProxy>} 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, y0; |
|
var xi, yi; |
|
var x, 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, |
|
|
|
/** |
|
* 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]); // 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]; |
|
} |
|
} |
|
}; |
|
|
|
// 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); |
|
} |
|
}); |
|
|
|
// 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$1 = 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] |
|
*/ |
|
|
|
function subPixelOptimizeLine$1(outputShape, inputShape, style) { |
|
var lineWidth = style && style.lineWidth; |
|
|
|
if (!inputShape || !lineWidth) { |
|
return; |
|
} |
|
|
|
var x1 = inputShape.x1; |
|
var x2 = inputShape.x2; |
|
var y1 = inputShape.y1; |
|
var y2 = inputShape.y2; |
|
|
|
if (round$1(x1 * 2) === round$1(x2 * 2)) { |
|
outputShape.x1 = outputShape.x2 = subPixelOptimize$1(x1, lineWidth, true); |
|
} else { |
|
outputShape.x1 = x1; |
|
outputShape.x2 = x2; |
|
} |
|
|
|
if (round$1(y1 * 2) === round$1(y2 * 2)) { |
|
outputShape.y1 = outputShape.y2 = subPixelOptimize$1(y1, lineWidth, true); |
|
} else { |
|
outputShape.y1 = y1; |
|
outputShape.y2 = y2; |
|
} |
|
} |
|
/** |
|
* 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] |
|
*/ |
|
|
|
function subPixelOptimizeRect$1(outputShape, inputShape, style) { |
|
var lineWidth = style && style.lineWidth; |
|
|
|
if (!inputShape || !lineWidth) { |
|
return; |
|
} |
|
|
|
var originX = inputShape.x; |
|
var originY = inputShape.y; |
|
var originWidth = inputShape.width; |
|
var originHeight = inputShape.height; |
|
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 Should be nonnegative integer. |
|
* @param {boolean=} positiveOrNegative Default false (negative). |
|
* @return {number} Optimized position. |
|
*/ |
|
|
|
function subPixelOptimize$1(position, lineWidth, positiveOrNegative) { |
|
// Assure that (position + lineWidth / 2) is near integer edge, |
|
// otherwise line will be fuzzy in canvas. |
|
var doubledPosition = round$1(position * 2); |
|
return (doubledPosition + round$1(lineWidth)) % 2 === 0 ? doubledPosition / 2 : (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2; |
|
} |
|
|
|
/** |
|
* 矩形 |
|
* @module zrender/graphic/shape/Rect |
|
*/ |
|
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 |
|
*/ |
|
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.<number>} |
|
*/ |
|
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.<number>} |
|
*/ |
|
pointAt: function (t) { |
|
return someVectorAt(this.shape, t, false); |
|
}, |
|
|
|
/** |
|
* Get tangent at percent |
|
* @param {number} t |
|
* @return {Array.<number>} |
|
*/ |
|
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]); |
|
} |
|
}, |
|
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.<Object>} 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.<Object>} 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.<Object>} [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. |
|
*/ |
|
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 round = Math.round; |
|
var mathMax$1 = Math.max; |
|
var mathMin$1 = Math.min; |
|
var EMPTY_OBJ = {}; |
|
var Z2_EMPHASIS_LIFT = 1; |
|
/** |
|
* Extend shape with parameters |
|
*/ |
|
|
|
function extendShape(opts) { |
|
return Path.extend(opts); |
|
} |
|
/** |
|
* Extend path |
|
*/ |
|
|
|
function extendPath(pathData, opts) { |
|
return extendFromString(pathData, opts); |
|
} |
|
/** |
|
* 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) { |
|
var shape = param.shape; |
|
var lineWidth = param.style.lineWidth; |
|
|
|
if (round(shape.x1 * 2) === round(shape.x2 * 2)) { |
|
shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true); |
|
} |
|
|
|
if (round(shape.y1 * 2) === round(shape.y2 * 2)) { |
|
shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true); |
|
} |
|
|
|
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) { |
|
var shape = param.shape; |
|
var lineWidth = param.style.lineWidth; |
|
var originX = shape.x; |
|
var originY = shape.y; |
|
var originWidth = shape.width; |
|
var originHeight = shape.height; |
|
shape.x = subPixelOptimize(shape.x, lineWidth, true); |
|
shape.y = subPixelOptimize(shape.y, lineWidth, true); |
|
shape.width = Math.max(subPixelOptimize(originX + originWidth, lineWidth, false) - shape.x, originWidth === 0 ? 0 : 1); |
|
shape.height = Math.max(subPixelOptimize(originY + originHeight, lineWidth, false) - shape.y, originHeight === 0 ? 0 : 1); |
|
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. |
|
*/ |
|
|
|
function subPixelOptimize(position, lineWidth, positiveOrNegative) { |
|
// 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; |
|
} |
|
|
|
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 `doSingleEnterHover`. |
|
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 doSingleEnterHover(el) { |
|
var hoverStl = el.__hoverStl; |
|
|
|
if (!hoverStl || el.__highlighted) { |
|
return; |
|
} |
|
|
|
var useHoverLayer = el.useHoverLayer; |
|
el.__highlighted = useHoverLayer ? 'layer' : 'plain'; |
|
var zr = el.__zr; |
|
|
|
if (!zr && 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, where |
|
// only properties that is not `null/undefined` can be set. The disadventage: |
|
// null/undefined can not be used to remove style any more in `emphasis`. |
|
|
|
|
|
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 doSingleLeaveHover(el) { |
|
var highlighted = el.__highlighted; |
|
|
|
if (!highlighted) { |
|
return; |
|
} |
|
|
|
el.__highlighted = false; |
|
|
|
if (highlighted === 'layer') { |
|
el.__zr && el.__zr.removeHover(el); |
|
} else if (highlighted) { |
|
var style = el.style; |
|
var normalStl = el.__cachedNormalStl; |
|
|
|
if (normalStl) { |
|
rollbackDefaultTextStyle(style); // Consider null/undefined value, should use |
|
// `setStyle` but not `extendFrom(stl, true)`. |
|
|
|
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 traverseCall(el, method) { |
|
el.isGroup ? el.traverse(function (child) { |
|
!child.isGroup && method(child); |
|
}) : method(el); |
|
} |
|
/** |
|
* 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|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. |
|
* @param {Object} [opt] |
|
* @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger` |
|
*/ |
|
|
|
|
|
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 && (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. |
|
|
|
doSingleLeaveHover(el); |
|
doSingleEnterHover(el); |
|
} |
|
} |
|
/** |
|
* Emphasis (called by API) has higher priority than `mouseover`. |
|
* When element has been called to be entered emphasis, mouse over |
|
* should not trigger the highlight effect (for example, animation |
|
* scale) again, and `mouseout` should not downplay the highlight |
|
* effect. So the listener of `mouseover` and `mouseout` should |
|
* check `isInEmphasis`. |
|
* |
|
* @param {module:zrender/Element} el |
|
* @return {boolean} |
|
*/ |
|
|
|
function isInEmphasis(el) { |
|
return el && el.__isEmphasisEntered; |
|
} |
|
|
|
function onElementMouseOver(e) { |
|
if (this.__hoverSilentOnTouch && e.zrByTouch) { |
|
return; |
|
} // Only if element is not in emphasis status |
|
|
|
|
|
!this.__isEmphasisEntered && traverseCall(this, doSingleEnterHover); |
|
} |
|
|
|
function onElementMouseOut(e) { |
|
if (this.__hoverSilentOnTouch && e.zrByTouch) { |
|
return; |
|
} // Only if element is not in emphasis status |
|
|
|
|
|
!this.__isEmphasisEntered && traverseCall(this, doSingleLeaveHover); |
|
} |
|
|
|
function enterEmphasis() { |
|
this.__isEmphasisEntered = true; |
|
traverseCall(this, doSingleEnterHover); |
|
} |
|
|
|
function leaveEmphasis() { |
|
this.__isEmphasisEntered = false; |
|
traverseCall(this, doSingleLeaveHover); |
|
} |
|
/** |
|
* Set hover style (namely "emphasis style") of element, |
|
* based on the current style of the given `el`. |
|
* |
|
* (1) |
|
* **CONSTRAINTS** for this method: |
|
* <A> This method MUST be called after all of the normal styles having been adopted |
|
* to the `el`. |
|
* <B> The input `hoverStyle` (that is, "emphasis style") MUST be the subset of the |
|
* "normal style" having been set to the el. |
|
* <C> `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. |
|
* |
|
* @param {module:zrender/Element} el |
|
* @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`. |
|
* @param {Object} [opt] |
|
* @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`. |
|
*/ |
|
|
|
|
|
function setHoverStyle(el, hoverStyle, opt) { |
|
el.isGroup ? el.traverse(function (child) { |
|
// If element has sepcified hoverStyle, then use it instead of given hoverStyle |
|
// Often used when item group has a label element and it's hoverStyle is different |
|
!child.isGroup && setElementHoverStyle(child, child.hoverStyle || hoverStyle); |
|
}) : setElementHoverStyle(el, el.hoverStyle || hoverStyle); |
|
setAsHoverStyleTrigger(el, opt); |
|
} |
|
/** |
|
* @param {Object|boolean} [opt] If `false`, means disable trigger. |
|
* @param {boolean} [opt.hoverSilentOnTouch=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, hoverSilentOnTouch should be used to disable hover-highlight |
|
* on touch device. |
|
*/ |
|
|
|
function setAsHoverStyleTrigger(el, opt) { |
|
var disable = opt === false; |
|
el.__hoverSilentOnTouch = opt != null && opt.hoverSilentOnTouch; // Simple optimize, since this method might be |
|
// called for each elements of a group in some cases. |
|
|
|
if (!disable || el.__hoverStyleTrigger) { |
|
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 |
|
|
|
el[method]('emphasis', enterEmphasis)[method]('normal', leaveEmphasis); |
|
el.__hoverStyleTrigger = !disable; |
|
} |
|
} |
|
/** |
|
* 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)` |
|
* @param {module:echarts/model/Model} [opt.labelDataIndex] Fetch text by |
|
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)` |
|
* @param {module:echarts/model/Model} [opt.labelDimIndex] Fetch text by |
|
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)` |
|
* @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; // 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); |
|
} |
|
|
|
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) : 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; |
|
} |
|
/** |
|
* 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 = 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`. |
|
|
|
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); // Save original textPosition, because style.textPosition will be repalced by |
|
// real location (like [10, 30]) in zrender. |
|
|
|
textStyle.insideRawTextPosition = textStyle.textPosition; |
|
|
|
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 opt = textStyle.insideRollbackOpt; // Only `insideRollbackOpt` created (in `setTextStyleCommon`), |
|
// applyDefaultTextStyle works. |
|
|
|
if (!opt || textStyle.textFill != null) { |
|
return; |
|
} |
|
|
|
var useInsideStyle = opt.useInsideStyle; |
|
var textPosition = textStyle.insideRawTextPosition; |
|
var insideRollback; |
|
var autoColor = opt.autoColor; |
|
|
|
if (useInsideStyle !== false && (useInsideStyle === true || opt.isRectText && textPosition // textPosition can be [10, 30] |
|
&& typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0)) { |
|
insideRollback = { |
|
textFill: null, |
|
textStroke: textStyle.textStroke, |
|
textStrokeWidth: textStyle.textStrokeWidth |
|
}; |
|
textStyle.textFill = '#fff'; // Consider text with #fff overflow its container. |
|
|
|
if (textStyle.textStroke == null) { |
|
textStyle.textStroke = autoColor; |
|
textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2); |
|
} |
|
} else if (autoColor != null) { |
|
insideRollback = { |
|
textFill: null |
|
}; |
|
textStyle.textFill = autoColor; |
|
} // Always set `insideRollback`, for clearing previous. |
|
|
|
|
|
if (insideRollback) { |
|
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) { |
|
style.textFill = insideRollback.textFill; |
|
style.textStroke = insideRollback.textStroke; |
|
style.textStrokeWidth = insideRollback.textStrokeWidth; |
|
style.insideRollback = null; |
|
} |
|
} |
|
|
|
function getFont(opt, ecModel) { |
|
// ecModel or default text style model. |
|
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 if 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 if 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.<number>} target [x, y] |
|
* @param {Array.<number>|TypedArray.<number>|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.<number>} [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.<number>} 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.<Array.<number>>} points Like: [[23, 44], [53, 66], ...] |
|
* @param {Object} rect {x, y, width, height} |
|
* @return {Array.<Array.<number>>} 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'); |
|
} |
|
} |
|
|
|
|
|
var graphic = (Object.freeze || Object)({ |
|
Z2_EMPHASIS_LIFT: Z2_EMPHASIS_LIFT, |
|
extendShape: extendShape, |
|
extendPath: extendPath, |
|
makePath: makePath, |
|
makeImage: makeImage, |
|
mergePath: mergePath, |
|
resizePath: resizePath, |
|
subPixelOptimizeLine: subPixelOptimizeLine, |
|
subPixelOptimizeRect: subPixelOptimizeRect, |
|
subPixelOptimize: subPixelOptimize, |
|
setElementHoverStyle: setElementHoverStyle, |
|
isInEmphasis: isInEmphasis, |
|
setHoverStyle: setHoverStyle, |
|
setAsHoverStyleTrigger: setAsHoverStyleTrigger, |
|
setLabelStyle: setLabelStyle, |
|
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, |
|
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.<string>} 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.<string>} [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>|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.<string>} targetNameList Target Component type list. |
|
* Can be ['aa', 'bb', 'aa.xx'] |
|
* @param {Array.<string>} 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 |
|
* </licenses/LICENSE-d3>). |
|
*/ |
|
var RADIAN_EPSILON = 1e-4; |
|
|
|
function _trim(str) { |
|
return str.replace(/^\s+/, '').replace(/\s+$/, ''); |
|
} |
|
/** |
|
* Linear mapping a value from domain to range |
|
* @memberOf module:echarts/util/number |
|
* @param {(number|Array.<number>)} val |
|
* @param {Array.<number>} domain Domain extent domain[0] can be bigger than domain[1] |
|
* @param {Array.<number>} range Range extent range[0] can be bigger than range[1] |
|
* @param {boolean} clamp |
|
* @return {(number|Array.<number>} |
|
*/ |
|
|
|
|
|
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$2(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; |
|
} |
|
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.<number>} dataExtent |
|
* @param {Array.<number>} 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.<number>} 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 <https://momentjs.com/>). |
|
* + 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)); |
|
} |
|
|
|
function quantityExponent(val) { |
|
return Math.floor(Math.log(val) / Math.LN10); |
|
} |
|
/** |
|
* 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" |
|
* <https://github.com/d3/d3/blob/9cc9a875e636a1dcf36cc1e07bdf77e1ad6e2c74/src/arrays/quantile.js>. |
|
* See the license statement at the head of this file. |
|
* @param {Array.<number>} 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.<Object>} list, where `close` mean open or close |
|
* of the interval, and Infinity can be used. |
|
* @return {Array.<Object>} 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$2, |
|
asc: asc, |
|
getPrecision: getPrecision, |
|
getPrecisionSafe: getPrecisionSafe, |
|
getPixelPrecision: getPixelPrecision, |
|
getPercentWithPrecision: getPercentWithPrecision, |
|
MAX_SAFE_INTEGER: MAX_SAFE_INTEGER, |
|
remRadian: remRadian, |
|
isRadianAroundZero: isRadianAroundZero, |
|
parseDate: parseDate, |
|
quantity: quantity, |
|
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. |
|
*/ |
|
/** |
|
* 每三位默认加,格式化 |
|
* @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>|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' ? '<span style="display:inline-block;vertical-align:middle;margin-right:8px;margin-left:3px;' + 'border-radius:4px;width:4px;height:4px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>' : '<span style="display:inline-block;margin-right:5px;' + 'border-radius:10px;width:10px;height:10px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>'; |
|
} 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.<number>} [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); |
|
} |
|
|
|
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 |
|
}); |
|
|
|
/* |
|
* 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.<number>} [opt.hv=[1,1]] Only horizontal or only vertical. |
|
* @param {Array.<number>} [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. |
|
*/ |
|
|
|
|
|
/** |
|
* @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.<boolean>} [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.<string, Array.<module:echarts/model/Model>>} |
|
* @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 <echarts/test/heatmap-large.html>. |
|
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. |
|
*/ |
|
|
|
/** |
|
* 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". |
|
/** |
|
* @return {Object} For example: |
|
* { |
|
* coordSysName: 'cartesian2d', |
|
* coordSysDims: ['x', 'y', ...], |
|
* axisMap: HashMap({ |
|
* x: xAxisModel, |
|
* y: yAxisModel |
|
* }), |
|
* categoryAxisMap: HashMap({ |
|
* x: xAxisModel, |
|
* y: undefined |
|
* }), |
|
* // It also indicate that whether there is category axis. |
|
* firstCategoryDimIndex: 1, |
|
* // To replace user specified encode. |
|
* } |
|
*/ |
|
|
|
function getCoordSysDefineBySeries(seriesModel) { |
|
var coordSysName = seriesModel.get('coordinateSystem'); |
|
var result = { |
|
coordSysName: coordSysName, |
|
coordSysDims: [], |
|
axisMap: createHashMap(), |
|
categoryAxisMap: createHashMap() |
|
}; |
|
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]; |
|
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 = 1; |
|
} |
|
}, |
|
singleAxis: function (seriesModel, result, axisMap, categoryAxisMap) { |
|
var singleAxisModel = seriesModel.getReferringComponents('singleAxis')[0]; |
|
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'); |
|
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 = 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. |
|
*/ |
|
// 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.<Object|string>} [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.<Object|string>} |
|
*/ |
|
|
|
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. |
|
*/ |
|
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); // Note: dataset option does not have `encode`. |
|
|
|
var encodeDefine = seriesOption.encode; |
|
|
|
if (!encodeDefine && datasetModel) { |
|
encodeDefine = makeDefaultEncode(seriesModel, datasetModel, data, sourceFormat, seriesLayoutBy, completeResult); |
|
} |
|
|
|
inner$3(seriesModel).source = new Source({ |
|
data: data, |
|
fromDataset: fromDataset, |
|
seriesLayoutBy: seriesLayoutBy, |
|
sourceFormat: sourceFormat, |
|
dimensionsDefine: completeResult.dimensionsDefine, |
|
startIndex: completeResult.startIndex, |
|
dimensionsDetectCount: completeResult.dimensionsDetectCount, |
|
encodeDefine: encodeDefine |
|
}); |
|
} // return {startIndex, dimensionsDefine, dimensionsCount} |
|
|
|
function completeBySourceData(data, sourceFormat, seriesLayoutBy, sourceHeader, dimensionsDefine) { |
|
if (!data) { |
|
return { |
|
dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine) |
|
}; |
|
} |
|
|
|
var dimensionsDetectCount; |
|
var startIndex; |
|
var findPotentialName; |
|
|
|
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); |
|
findPotentialName = true; |
|
} |
|
} else if (sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS) { |
|
if (!dimensionsDefine) { |
|
dimensionsDefine = []; |
|
findPotentialName = true; |
|
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) {} |
|
|
|
var potentialNameDimIndex; |
|
|
|
if (findPotentialName) { |
|
each$1(dimensionsDefine, function (dim, idx) { |
|
if ((isObject$1(dim) ? dim.name : dim) === 'name') { |
|
potentialNameDimIndex = idx; |
|
} |
|
}); |
|
} |
|
|
|
return { |
|
startIndex: startIndex, |
|
dimensionsDefine: normalizeDimensionsDefine(dimensionsDefine), |
|
dimensionsDetectCount: dimensionsDetectCount, |
|
potentialNameDimIndex: potentialNameDimIndex // TODO: potentialIdDimIdx |
|
|
|
}; |
|
} // 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; |
|
} |
|
} // ??? TODO merge to completedimensions, where also has |
|
// default encode making logic. And the default rule |
|
// should depends on series? consider 'map'. |
|
|
|
|
|
function makeDefaultEncode(seriesModel, datasetModel, data, sourceFormat, seriesLayoutBy, completeResult) { |
|
var coordSysDefine = getCoordSysDefineBySeries(seriesModel); |
|
var encode = {}; // var encodeTooltip = []; |
|
// var encodeLabel = []; |
|
|
|
var encodeItemName = []; |
|
var encodeSeriesName = []; |
|
var seriesType = seriesModel.subType; // ??? TODO refactor: provide by series itself. |
|
// Consider the case: 'map' series is based on geo coordSys, |
|
// 'graph', 'heatmap' can be based on cartesian. But can not |
|
// give default rule simply here. |
|
|
|
var nSeriesMap = createHashMap(['pie', 'map', 'funnel']); |
|
var cSeriesMap = createHashMap(['line', 'bar', 'pictorialBar', 'scatter', 'effectScatter', 'candlestick', 'boxplot']); // Usually in this case series will use the first data |
|
// dimension as the "value" dimension, or other default |
|
// processes respectively. |
|
|
|
if (coordSysDefine && cSeriesMap.get(seriesType) != null) { |
|
var ecModel = seriesModel.ecModel; |
|
var datasetMap = inner$3(ecModel).datasetMap; |
|
var key = datasetModel.uid + '_' + seriesLayoutBy; |
|
var datasetRecord = datasetMap.get(key) || datasetMap.set(key, { |
|
categoryWayDim: 1, |
|
valueWayDim: 0 |
|
}); // TODO |
|
// Auto detect first time axis and do arrangement. |
|
|
|
each$1(coordSysDefine.coordSysDims, function (coordDim) { |
|
// In value way. |
|
if (coordSysDefine.firstCategoryDimIndex == null) { |
|
var dataDim = datasetRecord.valueWayDim++; |
|
encode[coordDim] = dataDim; // ??? 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. |
|
|
|
encodeSeriesName.push(dataDim); // encodeTooltip.push(dataDim); |
|
// encodeLabel.push(dataDim); |
|
} // In category way, category axis. |
|
else if (coordSysDefine.categoryAxisMap.get(coordDim)) { |
|
encode[coordDim] = 0; |
|
encodeItemName.push(0); |
|
} // In category way, non-category axis. |
|
else { |
|
var dataDim = datasetRecord.categoryWayDim++; |
|
encode[coordDim] = dataDim; // encodeTooltip.push(dataDim); |
|
// encodeLabel.push(dataDim); |
|
|
|
encodeSeriesName.push(dataDim); |
|
} |
|
}); |
|
} // Do not make a complex rule! Hard to code maintain and not necessary. |
|
// ??? TODO refactor: provide by series itself. |
|
// [{name: ..., value: ...}, ...] like: |
|
else if (nSeriesMap.get(seriesType) != null) { |
|
// Find the first not ordinal. (5 is an experience value) |
|
var firstNotOrdinal; |
|
|
|
for (var i = 0; i < 5 && firstNotOrdinal == null; i++) { |
|
if (!doGuessOrdinal(data, sourceFormat, seriesLayoutBy, completeResult.dimensionsDefine, completeResult.startIndex, i)) { |
|
firstNotOrdinal = i; |
|
} |
|
} |
|
|
|
if (firstNotOrdinal != null) { |
|
encode.value = firstNotOrdinal; |
|
var nameDimIndex = completeResult.potentialNameDimIndex || Math.max(firstNotOrdinal - 1, 0); // By default, label use itemName in charts. |
|
// So we dont set encodeLabel here. |
|
|
|
encodeSeriesName.push(nameDimIndex); |
|
encodeItemName.push(nameDimIndex); // encodeTooltip.push(firstNotOrdinal); |
|
} |
|
} // encodeTooltip.length && (encode.tooltip = encodeTooltip); |
|
// encodeLabel.length && (encode.label = encodeLabel); |
|
|
|
|
|
encodeItemName.length && (encode.itemName = encodeItemName); |
|
encodeSeriesName.length && (encode.seriesName = encodeSeriesName); |
|
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 {boolean} Whether ordinal. |
|
*/ |
|
|
|
|
|
function guessOrdinal(source, dimIndex) { |
|
return doGuessOrdinal(source.data, source.sourceFormat, source.seriesLayoutBy, source.dimensionsDefine, source.startIndex, dimIndex); |
|
} // dimIndex may be overflow source data. |
|
|
|
function doGuessOrdinal(data, sourceFormat, seriesLayoutBy, dimensionsDefine, startIndex, dimIndex) { |
|
var result; // Experience value. |
|
|
|
var maxLoop = 5; |
|
|
|
if (isTypedArray(data)) { |
|
return false; |
|
} // When sourceType is 'objectRows' or 'keyedColumns', dimensionsDefine |
|
// always exists in source. |
|
|
|
|
|
var dimName; |
|
|
|
if (dimensionsDefine) { |
|
dimName = dimensionsDefine[dimIndex]; |
|
dimName = isObject$1(dimName) ? dimName.name : dimName; |
|
} |
|
|
|
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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
var sample = data[dimName]; |
|
|
|
if (!sample || isTypedArray(sample)) { |
|
return false; |
|
} |
|
|
|
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 false; |
|
} |
|
|
|
if ((result = detectValue(val[dimIndex])) != null) { |
|
return result; |
|
} |
|
} |
|
} |
|
|
|
function detectValue(val) { |
|
// Consider usage convenience, '1', '2' will be treated as "number". |
|
// `isFinit('')` get `true`. |
|
if (val != null && isFinite(val) && val !== '') { |
|
return false; |
|
} else if (isString(val) && val !== '-') { |
|
return true; |
|
} |
|
} |
|
|
|
return 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. |
|
*/ |
|
|
|
/** |
|
* 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 instanceof 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.<number>} [condition.index] Either input index or id or name. |
|
* @param {string|Array.<string>} [condition.id] Either input index or id or name. |
|
* @param {string|Array.<string>} [condition.name] Either input index or id or name. |
|
* @return {Array.<module:echarts/model/Component>} |
|
*/ |
|
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'}, |
|
* 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.<module:echarts/model/Component>} |
|
*/ |
|
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.<module:echarts/model/Series>} |
|
*/ |
|
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.<module:echarts/model/Series>} |
|
*/ |
|
getSeriesByType: function (subType) { |
|
var series = this._componentsMap.get('series'); |
|
|
|
return filter(series, function (oneSeries) { |
|
return oneSeries.subType === subType; |
|
}); |
|
}, |
|
|
|
/** |
|
* @return {Array.<module:echarts/model/Series>} |
|
*/ |
|
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) { |
|
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. |
|
* |
|
* @parma {string} subType |
|
* @param {Function} cb |
|
* @param {*} context |
|
*/ |
|
eachSeriesByType: function (subType, cb, context) { |
|
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) { |
|
return this._seriesIndicesMap.get(seriesModel.componentIndex) == null; |
|
}, |
|
|
|
/** |
|
* @return {Array.<number>} |
|
*/ |
|
getCurrentSeriesIndices: function () { |
|
return (this._seriesIndices || []).slice(); |
|
}, |
|
|
|
/** |
|
* @param {Function} cb |
|
* @param {*} context |
|
*/ |
|
filterSeries: function (cb, context) { |
|
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.<string, Array.<module:echarts/model/Model>>} |
|
* @private |
|
*/ |
|
|
|
this._componentsMap = createHashMap({ |
|
series: [] |
|
}); |
|
/** |
|
* Mapping between filtered series list and raw series list. |
|
* key: filtered series indices, value: raw series indices. |
|
* @type {Array.<nubmer>} |
|
* @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>|string} types model types |
|
* @return {Object} key: {string} type, value: {Array.<Object>} 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; |
|
} |
|
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.<number>} |
|
*/ |
|
|
|
this._timelineOptions = []; |
|
/** |
|
* @private |
|
* @type {Array.<Object>} |
|
*/ |
|
|
|
this._mediaList = []; |
|
/** |
|
* @private |
|
* @type {Object} |
|
*/ |
|
|
|
this._mediaDefault; |
|
/** |
|
* -1, means default. |
|
* empty means no media. |
|
* @private |
|
* @type {Array.<number>} |
|
*/ |
|
|
|
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.<Function>} 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, true); // 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.<Object>} |
|
*/ |
|
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 <http://www.w3.org/TR/css3-mediaqueries/#media1> |
|
* 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 === 'pie' || seriesType === 'gauge') { |
|
if (seriesOpt.clockWise != null) { |
|
seriesOpt.clockwise = seriesOpt.clockWise; |
|
} |
|
} |
|
|
|
if (seriesType === 'gauge') { |
|
var pointerColor = get(seriesOpt, 'pointer.color'); |
|
pointerColor != null && set$1(seriesOpt, 'itemStyle.normal.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. |
|
*/ |
|
// data processing stage is blocked in stream. |
|
// See <module:echarts/stream/Scheduler#performDataProcessorTasks> |
|
// (2) Only register once when import repeatly. |
|
// Should be executed before 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) { |
|
this._offset = 0; |
|
this._dimSize = dimSize; |
|
this._data = data; |
|
} |
|
|
|
var methods = providerMethods[sourceFormat === SOURCE_FORMAT_ARRAY_ROWS ? sourceFormat + '_' + source.seriesLayoutBy : 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) { |
|
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.<number>|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' |
|
*/ |
|
|
|
/* |
|
* 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 tooltipModel = this.ecModel.getComponent('tooltip'); |
|
var renderModeOption = tooltipModel && tooltipModel.get('renderMode'); |
|
var renderMode = getTooltipRenderMode(renderModeOption); |
|
var mainType = this.mainType; |
|
var isSeries = mainType === 'series'; |
|
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, |
|
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] |
|
* @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; |
|
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) { |
|
this._dueEnd = upTask._outputDueEnd; |
|
} // DataTask or overallTask |
|
else { |
|
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; |
|
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 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, |
|
|
|
/** |
|
* Data provided for legend |
|
* @type {Function} |
|
*/ |
|
// PENDING |
|
legendDataProvider: null, |
|
|
|
/** |
|
* Access path of color for visual |
|
*/ |
|
visualColorAccessPath: 'itemStyle.color', |
|
|
|
/** |
|
* 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; |
|
|
|
/** |
|
* @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' ? '<br/>' : '\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' : '<br/>' : ''; |
|
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>|string} dim |
|
* @param {Array.<number>} value |
|
* @param {module:echarts/coord/single/SingleAxis} baseAxis |
|
* @return {Object} {dataIndices, nestestValue}. |
|
*/ |
|
getAxisTooltipData: null, |
|
|
|
/** |
|
* See tooltip. |
|
* @abstract |
|
* @param {number} dataIndex |
|
* @return {Array.<number>} 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 (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; |
|
var large = fields.large = pipelineContext.large; |
|
var progressive = fields.progressiveRender = 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.<number>} 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 |
|
*/ |
|
|
|
|
|
function elSetState(el, state) { |
|
if (el) { |
|
el.trigger(state); |
|
|
|
if (el.type === 'group') { |
|
for (var i = 0; i < el.childCount(); i++) { |
|
elSetState(el.childAt(i), state); |
|
} |
|
} |
|
} |
|
} |
|
/** |
|
* @param {module:echarts/data/List} data |
|
* @param {Object} payload |
|
* @param {string} state 'normal'|'emphasis' |
|
*/ |
|
|
|
|
|
function toggleHighlight(data, payload, state) { |
|
var dataIndex = queryDataIndex(data, payload); |
|
|
|
if (dataIndex != null) { |
|
each$1(normalizeToArray(dataIndex), function (dataIdx) { |
|
elSetState(data.getItemGraphicEl(dataIdx), state); |
|
}); |
|
} else { |
|
data.eachItemGraphicEl(function (el) { |
|
elSetState(el, state); |
|
}); |
|
} |
|
} // 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 |
|
*/ |
|
|
|
/* |
|
* 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('.'); |
|
var color = seriesModel.get(colorAccessPath) // Set in itemStyle |
|
|| seriesModel.getColorFromPalette( // TODO series count changed. |
|
seriesModel.name, null, ecModel.getSeriesCount()); // Default color |
|
// FIXME Set color function or use the platte color |
|
|
|
data.setVisual('color', color); // Only visible series has each data be visual encoded |
|
|
|
if (!ecModel.isSeriesFiltered(seriesModel)) { |
|
if (typeof color === 'function' && !(color instanceof Gradient)) { |
|
data.each(function (idx) { |
|
data.setItemVisual(idx, 'color', color(seriesModel.getDataParams(idx))); |
|
}); |
|
} // itemStyle in each data item |
|
|
|
|
|
var dataEach = function (data, idx) { |
|
var itemModel = data.getItemModel(idx); |
|
var color = itemModel.get(colorAccessPath, true); |
|
|
|
if (color != null) { |
|
data.setItemVisual(idx, 'color', color); |
|
} |
|
}; |
|
|
|
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. |
|
*/ |
|
var lang = { |
|
toolbox: { |
|
brush: { |
|
title: { |
|
rect: '矩形选择', |
|
polygon: '圈选', |
|
lineX: '横向选择', |
|
lineY: '纵向选择', |
|
keep: '保持选择', |
|
clear: '清除选择' |
|
} |
|
}, |
|
dataView: { |
|
title: '数据视图', |
|
lang: ['数据视图', '关闭', '刷新'] |
|
}, |
|
dataZoom: { |
|
title: { |
|
zoom: '区域缩放', |
|
back: '区域缩放还原' |
|
} |
|
}, |
|
magicType: { |
|
title: { |
|
line: '切换为折线图', |
|
bar: '切换为柱状图', |
|
stack: '切换为堆叠', |
|
tiled: '切换为平铺' |
|
} |
|
}, |
|
restore: { |
|
title: '还原' |
|
}, |
|
saveAsImage: { |
|
title: '保存为图片', |
|
lang: ['右键另存为图片'] |
|
} |
|
}, |
|
series: { |
|
typeNames: { |
|
pie: '饼图', |
|
bar: '柱状图', |
|
line: '折线图', |
|
scatter: '散点图', |
|
effectScatter: '涟漪散点图', |
|
radar: '雷达图', |
|
tree: '树图', |
|
treemap: '矩形树图', |
|
boxplot: '箱型图', |
|
candlestick: 'K线图', |
|
k: 'K线图', |
|
heatmap: '热力图', |
|
map: '地图', |
|
parallel: '平行坐标图', |
|
lines: '线图', |
|
graph: '关系图', |
|
sankey: '桑基图', |
|
funnel: '漏斗图', |
|
gauge: '仪表盘图', |
|
pictorialBar: '象形柱图', |
|
themeRiver: '主题河流图', |
|
sunburst: '旭日图' |
|
} |
|
}, |
|
aria: { |
|
general: { |
|
withTitle: '这是一个关于“{title}”的图表。', |
|
withoutTitle: '这是一个图表,' |
|
}, |
|
series: { |
|
single: { |
|
prefix: '', |
|
withName: '图表类型是{seriesType},表示{seriesName}。', |
|
withoutName: '图表类型是{seriesType}。' |
|
}, |
|
multiple: { |
|
prefix: '它由{seriesCount}个图表系列组成。', |
|
withName: '第{seriesId}个系列是一个表示{seriesName}的{seriesType},', |
|
withoutName: '第{seriesId}个系列是一个{seriesType},', |
|
separator: { |
|
middle: ';', |
|
end: '。' |
|
} |
|
} |
|
}, |
|
data: { |
|
allData: '其数据是——', |
|
partialData: '其中,前{displayCnt}项是——', |
|
withName: '{name}的数据是{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', |
|
color: '#c23531', |
|
textColor: '#000', |
|
maskColor: 'rgba(255, 255, 255, 0.8)', |
|
zlevel: 0 |
|
}); |
|
var mask = new Rect({ |
|
style: { |
|
fill: opts.maskColor |
|
}, |
|
zlevel: opts.zlevel, |
|
z: 10000 |
|
}); |
|
var arc = new Arc({ |
|
shape: { |
|
startAngle: -PI$1 / 2, |
|
endAngle: -PI$1 / 2 + 0.1, |
|
r: 10 |
|
}, |
|
style: { |
|
stroke: opts.color, |
|
lineCap: 'round', |
|
lineWidth: 5 |
|
}, |
|
zlevel: opts.zlevel, |
|
z: 10001 |
|
}); |
|
var labelRect = new Rect({ |
|
style: { |
|
fill: 'none', |
|
text: opts.text, |
|
textPosition: 'right', |
|
textDistance: 10, |
|
textFill: opts.textColor |
|
}, |
|
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'); |
|
var group = new Group(); |
|
group.add(arc); |
|
group.add(labelRect); |
|
group.add(mask); // Inject resize |
|
|
|
group.resize = function () { |
|
var cx = api.getWidth() / 2; |
|
var cy = api.getHeight() / 2; |
|
arc.setShape({ |
|
cx: cx, |
|
cy: cy |
|
}); |
|
var r = arc.shape.r; |
|
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 restroe 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); |
|
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 |
|
} |
|
} |
|
}, |
|
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 Pattern from '../graphic/Pattern'; |
|
|
|
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 <!DOCTYPE svg> 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 <style>...</style> 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.<number>} 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]; |
|
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.2.1'; |
|
var dependencies = { |
|
zrender: '4.0.6' |
|
}; |
|
var TEST_FRAME_REMAIN_TIME = 1; |
|
var PRIORITY_PROCESSOR_FILTER = 1000; |
|
var PRIORITY_PROCESSOR_STATISTIC = 5000; |
|
var PRIORITY_VISUAL_LAYOUT = 1000; |
|
var PRIORITY_VISUAL_GLOBAL = 2000; |
|
var PRIORITY_VISUAL_CHART = 3000; |
|
var PRIORITY_VISUAL_COMPONENT = 4000; // FIXME |
|
// necessary? |
|
|
|
var PRIORITY_VISUAL_BRUSH = 5000; |
|
var PRIORITY = { |
|
PROCESSOR: { |
|
FILTER: PRIORITY_PROCESSOR_FILTER, |
|
STATISTIC: PRIORITY_PROCESSOR_STATISTIC |
|
}, |
|
VISUAL: { |
|
LAYOUT: PRIORITY_VISUAL_LAYOUT, |
|
GLOBAL: PRIORITY_VISUAL_GLOBAL, |
|
CHART: PRIORITY_VISUAL_CHART, |
|
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) { |
|
return function (eventName, handler, context) { |
|
// 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'); |
|
MessageCenter.prototype.off = createRegisterEventWithLowercaseName('off'); |
|
MessageCenter.prototype.one = createRegisterEventWithLowercaseName('one'); |
|
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'; |
|
|
|
/** |
|
* @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 pfs. |
|
* @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.<module:echarts/view/Chart>} |
|
* @private |
|
*/ |
|
|
|
this._chartsViews = []; |
|
/** |
|
* @type {Object.<string, module:echarts/view/Chart>} |
|
* @private |
|
*/ |
|
|
|
this._chartsMap = {}; |
|
/** |
|
* @type {Array.<module:echarts/view/Component>} |
|
* @private |
|
*/ |
|
|
|
this._componentsViews = []; |
|
/** |
|
* @type {Object.<string, module:echarts/view/Component>} |
|
* @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) { |
|
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(null, null, theme$$1, optionManager); |
|
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.pathToDataUrl(); |
|
}; |
|
/** |
|
* @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) { |
|
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 (!env$1.canvasSupported) { |
|
return; |
|
} |
|
|
|
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 = 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(); |
|
targetCanvas.width = width; |
|
targetCanvas.height = height; |
|
var zr = init$1(targetCanvas); |
|
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) { |
|
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; |
|
} |
|
} |
|
} |
|
/** |
|
* 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) { |
|
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 {} |
|
} else {} |
|
}, 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; |
|
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) { |
|
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 (isObject(name)) { |
|
cfg = name; |
|
name = ''; |
|
} |
|
|
|
name = name || 'default'; |
|
this.hideLoading(); |
|
|
|
if (!loadingEffects[name]) { |
|
return; |
|
} |
|
|
|
var el = loadingEffects[name](this._api, cfg); |
|
var zr = this._zr; |
|
this._loadingFX = el; |
|
zr.add(el); |
|
}; |
|
/** |
|
* Hide loading effect |
|
*/ |
|
|
|
|
|
echartsProto.hideLoading = function () { |
|
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 not flush. |
|
* undefined: Auto decide whether perform flush. |
|
*/ |
|
|
|
|
|
echartsProto.dispatchAction = function (payload, opt) { |
|
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) { |
|
var seriesIndex = params.seriesIndex; |
|
var ecModel = this.getModel(); |
|
var seriesModel = ecModel.getSeriesByIndex(seriesIndex); |
|
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'); |
|
echartsProto.off = createRegisterEventWithLowercaseName('off'); |
|
echartsProto.one = createRegisterEventWithLowercaseName('one'); |
|
/** |
|
* 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); |
|
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._zr, 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]; |
|
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 () { |
|
this.setOption({ |
|
series: [] |
|
}, true); |
|
}; |
|
/** |
|
* Dispose instance |
|
*/ |
|
|
|
|
|
echartsProto.dispose = function () { |
|
if (this._disposed) { |
|
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 updateHoverLayerStatus(zr, ecModel) { |
|
var storage = zr.storage; |
|
var elCount = 0; |
|
storage.traverse(function (el) { |
|
if (!el.isGroup) { |
|
elCount++; |
|
} |
|
}); |
|
|
|
if (elCount > ecModel.get('hoverLayerThreshold') && !env$1.node) { |
|
storage.traverse(function (el) { |
|
if (!el.isGroup) { |
|
// 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; |
|
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.<Object.<string, Function>>} |
|
* @inner |
|
*/ |
|
|
|
var dataProcessorFuncs = []; |
|
/** |
|
* @type {Array.<Function>} |
|
* @inner |
|
*/ |
|
|
|
var optionPreprocessorFuncs = []; |
|
/** |
|
* @type {Array.<Function>} |
|
* @inner |
|
*/ |
|
|
|
var postUpdateFuncs = []; |
|
/** |
|
* Visual encoding functions of each stage |
|
* @type {Array.<Object.<string, Function>>} |
|
*/ |
|
|
|
var visualFuncs = []; |
|
/** |
|
* Theme storage |
|
* @type {Object.<key, 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] Currently only 'canvas' is supported. |
|
* @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) { |
|
var existInstance = getInstanceByDom(dom); |
|
|
|
if (existInstance) { |
|
return existInstance; |
|
} |
|
|
|
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.<module:echarts~ECharts>} 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.<string|Object>} |
|
*/ |
|
|
|
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; |
|
} |
|
|
|
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>|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_STATISTIC, 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); // Travel by inverted order to make sure order consistency |
|
// when duplicate keys exists (consider newDataIndex.pop() below). |
|
// For performance consideration, these code below do not look neat. |
|
|
|
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.unshift(); |
|
} 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 = []; |
|
each$1(data.dimensions, function (dimName) { |
|
var dimItem = data.getDimensionInfo(dimName); |
|
var coordDim = dimItem.coordDim; |
|
|
|
if (coordDim) { |
|
var coordDimArr = encode[coordDim]; |
|
|
|
if (!encode.hasOwnProperty(coordDim)) { |
|
coordDimArr = encode[coordDim] = []; |
|
} |
|
|
|
coordDimArr[dimItem.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; |
|
} |
|
} |
|
|
|
if (dimItem.defaultTooltip) { |
|
defaultedTooltip.push(dimName); |
|
} |
|
} |
|
|
|
OTHER_DIMENSIONS.each(function (v, otherDim) { |
|
var otherDimArr = encode[otherDim]; |
|
|
|
if (!encode.hasOwnProperty(otherDim)) { |
|
otherDimArr = encode[otherDim] = []; |
|
} |
|
|
|
var dimIndex = dimItem.otherDims[otherDim]; |
|
|
|
if (dimIndex != null && dimIndex !== false) { |
|
otherDimArr[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 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. |
|
*/ |
|
|
|
/* 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.<string|Object>} dimensions |
|
* For example, ['someDimName', {name: 'someDimName', type: 'someDimType'}, ...]. |
|
* Dimensions should be concrete names like x, y, z, lng, lat, angle, radius |
|
* Spetial fields: { |
|
* ordinalMeta: <module:echarts/data/OrdinalMeta> |
|
* createInvertedIndices: <boolean> |
|
* } |
|
* @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 = { |
|
name: 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.<string>} |
|
*/ |
|
|
|
|
|
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.<number>} |
|
* @readOnly |
|
*/ |
|
|
|
this._indices = null; |
|
this._count = 0; |
|
this._rawCount = 0; |
|
/** |
|
* Data storage |
|
* @type {Object.<key, Array.<TypedArray|Array>>} |
|
* @private |
|
*/ |
|
|
|
this._storage = {}; |
|
/** |
|
* @type {Array.<string>} |
|
*/ |
|
|
|
this._nameList = []; |
|
/** |
|
* @type {Array.<string>} |
|
*/ |
|
|
|
this._idList = []; |
|
/** |
|
* Models of data option is stored sparse for optimizing memory cost |
|
* @type {Array.<module:echarts/model/Model>} |
|
* @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.<Object>} |
|
* @private |
|
*/ |
|
|
|
this._itemVisuals = []; |
|
/** |
|
* Key: visual type, Value: boolean |
|
* @type {Object} |
|
* @readOnly |
|
*/ |
|
|
|
this.hasItemVisual = {}; |
|
/** |
|
* Item layout properties after layout |
|
* @type {Array.<Object>} |
|
* @private |
|
*/ |
|
|
|
this._itemLayouts = []; |
|
/** |
|
* Graphic elemnents |
|
* @type {Array.<module:zrender/Element>} |
|
* @private |
|
*/ |
|
|
|
this._graphicEls = []; |
|
/** |
|
* Max size of each chunk. |
|
* @type {number} |
|
* @private |
|
*/ |
|
|
|
this._chunkSize = 1e5; |
|
/** |
|
* @type {number} |
|
* @private |
|
*/ |
|
|
|
this._chunkCount = 0; |
|
/** |
|
* @type {Array.<Array|Object>} |
|
* @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.<number>} |
|
* @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.<Array|TypedArray>} |
|
* @private |
|
*/ |
|
|
|
this._invertedIndicesMap = invertedIndicesMap; |
|
/** |
|
* @type {Object} |
|
* @private |
|
*/ |
|
|
|
this._calculationInfo = {}; |
|
}; |
|
|
|
var listProto = List.prototype; |
|
listProto.type = 'list'; |
|
/** |
|
* If each data item has it's own option |
|
* @type {boolean} |
|
*/ |
|
|
|
listProto.hasItemOption = true; |
|
/** |
|
* Get dimension name |
|
* @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' |
|
* @return {string} Concrete dim name. |
|
*/ |
|
|
|
listProto.getDimension = function (dim) { |
|
if (!isNaN(dim)) { |
|
dim = this.dimensions[dim] || 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.<string>} 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.<string>} 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.<Object|number|Array>} data source or data or data provider. |
|
* @param {Array.<string>} [nameLIst] The name of a datum is used on data diff and |
|
* defualt 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); |
|
} |
|
|
|
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) { |
|
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.<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.<string>} [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.<string>} [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; |
|
var dimensionInfos = this._dimensionInfos; |
|
|
|
for (var i = 0, len = dataDimsOnCoord.length; i < len; i++) { |
|
if ( // Ordinal type can be string or number |
|
dimensionInfos[dataDimsOnCoord[i]].type !== 'ordinal' // FIXME check ordinal when using index? |
|
&& 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]; |
|
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 (!this._indices) { |
|
return rawIndex; |
|
} |
|
|
|
if (rawIndex >= this._rawCount || rawIndex < 0) { |
|
return -1; |
|
} // 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.<number>} Considere multiple points has the same value. |
|
*/ |
|
|
|
|
|
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 = Number.MAX_VALUE; |
|
var minDiff = -1; |
|
|
|
for (var i = 0, len = this.count(); i < len; i++) { |
|
var diff = value - this.get(dim, i |
|
/*, stack */ |
|
); |
|
var dist = Math.abs(diff); |
|
|
|
if (diff <= maxDistance && dist <= minDist) { |
|
// For the case of two data are same on xAxis, which has sequence data. |
|
// Show the nearest index |
|
// https://github.com/ecomfe/echarts/issues/2869 |
|
if (dist < minDist || diff >= 0 && minDiff < 0) { |
|
minDist = dist; |
|
minDiff = diff; |
|
nearestIndices.length = 0; |
|
} |
|
|
|
nearestIndices.push(i); |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
/** |
|
* Data iteration |
|
* @param {string|Array.<string>} |
|
* @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); |
|
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.<string>} |
|
* @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); |
|
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); |
|
} |
|
} |
|
|
|
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.<string>} [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.<string>} 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); |
|
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} |
|
* |
|
* Complete the dimensions array, by user defined `dimension` and `encode`, |
|
* and guessing from the data structure. |
|
* If no 'value' dimension specified, the first no-named dimension will be |
|
* named as 'value'. |
|
* |
|
* @param {Array.<string>} 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.<Object|string>} [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 {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. |
|
* @param {number} [opt.encodeDefaulter] If not specified, auto find the next available data dim. |
|
* @return {Array.<Object>} [{ |
|
* name: string mandatory, |
|
* displayName: string, the origin name in dimsDef, see source helper. |
|
* If displayName given, the tooltip will displayed vertically. |
|
* coordDim: string mandatory, |
|
* coordDimIndex: number mandatory, |
|
* type: string optional, |
|
* otherDims: { never null/undefined |
|
* tooltip: number optional, |
|
* label: number optional, |
|
* itemName: number optional, |
|
* seriesName: number optional, |
|
* }, |
|
* isExtraCoord: boolean true if coord is generated |
|
* (not specified in encode and not series specified) |
|
* other props ... |
|
* }] |
|
*/ |
|
|
|
function completeDimensions(sysDims, source, opt) { |
|
if (!Source.isInstance(source)) { |
|
source = Source.seriesDataToSource(source); |
|
} |
|
|
|
opt = opt || {}; |
|
sysDims = (sysDims || []).slice(); |
|
var dimsDef = (opt.dimsDef || []).slice(); |
|
var encodeDef = createHashMap(opt.encodeDef); |
|
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] = { |
|
otherDims: {} |
|
}; // 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); |
|
} // 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 && 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] || {}; |
|
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)) { |
|
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.<string|Object>} [opt.coordDimensions=[]] |
|
* @param {number} [opt.dimensionsCount] |
|
* @param {string} [opt.generateCoord] |
|
* @param {string} [opt.generateCoordCount] |
|
* @param {Array.<string|Object>} [opt.dimensionsDefine=source.dimensionsDefine] Overwrite source define. |
|
* @param {Object|HashMap} [opt.encodeDefine=source.encodeDefine] Overwrite source define. |
|
* @return {Array.<Object>} 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, |
|
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. |
|
*/ |
|
/** |
|
* 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.<string|Object>} dimensionInfoList The same as the input of <module:echarts/data/List>. |
|
* 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] |
|
*/ |
|
|
|
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 coordSysDefine = getCoordSysDefineBySeries(seriesModel); |
|
var coordSysDimDefs; |
|
|
|
if (coordSysDefine) { |
|
coordSysDimDefs = map(coordSysDefine.coordSysDims, function (dim) { |
|
var dimInfo = { |
|
name: dim |
|
}; |
|
var axisModel = coordSysDefine.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 |
|
}); |
|
var firstCategoryDimIndex; |
|
var hasNameEncode; |
|
coordSysDefine && each$1(dimInfoList, function (dimInfo, dimIndex) { |
|
var coordDim = dimInfo.coordDim; |
|
var categoryAxisModel = coordSysDefine.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.<number>} |
|
* @protected |
|
*/ |
|
|
|
this._extent = [Infinity, -Infinity]; |
|
/** |
|
* Step is calculated in adjustExtent |
|
* @type {Array.<number>} |
|
* @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.<number>} 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.<number>} |
|
*/ |
|
|
|
|
|
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.<string>} |
|
*/ |
|
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.<string>} 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$2; |
|
/** |
|
* @param {Array.<number>} 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]; |
|
} |
|
} |
|
function intervalScaleGetTicks(interval, extent, niceTickExtent, 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]) { |
|
ticks.push(extent[0]); |
|
} |
|
|
|
var tick = niceTickExtent[0]; |
|
|
|
while (tick <= niceTickExtent[1]) { |
|
ticks.push(tick); // Avoid rounding error |
|
|
|
tick = roundNumber$1(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]. |
|
|
|
|
|
if (extent[1] > (ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1])) { |
|
ticks.push(extent[1]); |
|
} |
|
|
|
return ticks; |
|
} |
|
|
|
/* |
|
* 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$2; |
|
/** |
|
* @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); |
|
}, |
|
|
|
/** |
|
* @return {Array.<number>} |
|
*/ |
|
getTicks: function () { |
|
return intervalScaleGetTicks(this._interval, this._extent, this._niceExtent, this._intervalPrecision); |
|
}, |
|
|
|
/** |
|
* @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.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; |
|
} |
|
function makeColumnLayout(barSeries) { |
|
var seriesInfoList = []; |
|
each$1(barSeries, function (seriesModel) { |
|
var data = seriesModel.getData(); |
|
var cartesian = seriesModel.coordinateSystem; |
|
var baseAxis = cartesian.getBaseAxis(); |
|
var axisExtent = baseAxis.getExtent(); |
|
var bandWidth = baseAxis.type === 'category' ? baseAxis.getBandWidth() : 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 barGap = seriesModel.get('barGap'); |
|
var barCategoryGap = seriesModel.get('barCategoryGap'); |
|
seriesInfoList.push({ |
|
bandWidth: bandWidth, |
|
barWidth: barWidth, |
|
barMaxWidth: barMaxWidth, |
|
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. |
|
// TODO |
|
|
|
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 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, stack) { |
|
var maxWidth = column.maxWidth; |
|
|
|
if (maxWidth && maxWidth < autoWidth) { |
|
maxWidth = Math.min(maxWidth, remainedWidth); |
|
|
|
if (column.width) { |
|
maxWidth = Math.min(maxWidth, column.width); |
|
} |
|
|
|
remainedWidth -= maxWidth; |
|
column.width = maxWidth; |
|
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] || { |
|
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 |
|
*/ |
|
|
|
// 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 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 largePoints = new LargeArr(params.count * 2); |
|
var dataIndex; |
|
var coord = []; |
|
var valuePair = []; |
|
var offset = 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); |
|
largePoints[offset++] = coord[0]; |
|
largePoints[offset++] = coord[1]; |
|
} |
|
|
|
data.setLayout({ |
|
largePoints: largePoints, |
|
barWidth: barWidth, |
|
valueAxisStart: getValueAxisStart(baseAxis, valueAxis, false), |
|
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) { |
|
var extent = valueAxis.getGlobalExtent(); |
|
var min; |
|
var max; |
|
|
|
if (extent[0] > extent[1]) { |
|
min = extent[1]; |
|
max = extent[0]; |
|
} else { |
|
min = extent[0]; |
|
max = extent[1]; |
|
} |
|
|
|
var valueStart = valueAxis.toGlobalCoord(valueAxis.dataToCoord(0)); |
|
valueStart < min && (valueStart = min); |
|
valueStart > max && (valueStart = max); |
|
return valueStart; |
|
} |
|
|
|
/* |
|
* 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 |
|
* </licenses/LICENSE-d3>). |
|
*/ |
|
// [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$2(mathFloor(extent[0] / interval) * interval); |
|
} |
|
|
|
if (!opt.fixMax) { |
|
extent[1] = round$2(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" |
|
* <https://github.com/d3/d3/blob/b516d77fb8566b576088e73410437494717ada26/src/time/scale.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 |
|
*/ |
|
var scaleProto$1 = Scale.prototype; |
|
var intervalScaleProto$1 = IntervalScale.prototype; |
|
var getPrecisionSafe$1 = getPrecisionSafe; |
|
var roundingErrorFix = round$2; |
|
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(); |
|
}, |
|
|
|
/** |
|
* @return {Array.<number>} |
|
*/ |
|
getTicks: function () { |
|
var originalScale = this._originalScale; |
|
var extent = this._extent; |
|
var originalExtent = originalScale.getExtent(); |
|
return map(intervalScaleProto$1.getTicks.call(this), function (val) { |
|
var powVal = round$2(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} 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.<number>} 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$2(mathCeil$1(extent[0] / interval) * interval), round$2(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 fixMin = min != null; |
|
var fixMax = max != null; |
|
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') { |
|
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 == 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; |
|
} |
|
|
|
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] |
|
}); |
|
} |
|
|
|
(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 [min, max]; |
|
} |
|
|
|
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 extent = getScaleExtent(scale, model); |
|
var fixMin = model.getMin() != null; |
|
var fixMax = model.getMax() != null; |
|
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: fixMin, |
|
fixMax: 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.cos(rotateRadians) + beforeHeight * Math.sin(rotateRadians); |
|
var afterHeight = beforeWidth * Math.sin(rotateRadians) + 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. |
|
*/ |
|
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.<string, module:zrender/graphic/Path>} |
|
*/ |
|
|
|
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 |
|
}, |
|
beforeBrush: function () { |
|
var style = this.style; |
|
var shape = this.shape; // FIXME |
|
|
|
if (shape.symbolType === 'pin' && style.textPosition === 'inside') { |
|
style.textPosition = ['50%', '40%']; |
|
style.textAlign = 'center'; |
|
style.textVerticalAlign = 'middle'; |
|
} |
|
}, |
|
buildPath: function (ctx, shape, inBundle) { |
|
var symbolType = shape.symbolType; |
|
var proxySymbol = symbolBuildProxies[symbolType]; |
|
|
|
if (shape.symbolType !== 'none') { |
|
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. |
|
*/ |
|
/** |
|
* 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); |
|
} // export function createGraph(seriesModel) { |
|
var dataStack$1 = { |
|
isDimensionStacked: isDimensionStacked, |
|
enableDataStack: enableDataStack, |
|
getStackedDimension: getStackedDimension |
|
}; |
|
/** |
|
* Create scale |
|
* @param {Array.<number>} 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.<string>` |
|
* `getCategories() => Array.<string>` |
|
* `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.<number>} cp |
|
*/ |
|
|
|
function Region(name, geometries, cp) { |
|
/** |
|
* @type {string} |
|
* @readOnly |
|
*/ |
|
this.name = name; |
|
/** |
|
* @type {Array.<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.<number>} |
|
*/ |
|
|
|
|
|
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 {<Array.<number>} 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 |
|
* @return {module:zrender/container/Group} |
|
*/ |
|
|
|
|
|
var parseGeoJSON = function (geoJson) { |
|
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.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.<number> |
|
* 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 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. |
|
|
|
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) { |
|
interval = lastAutoInterval; |
|
} // Only update cache if cache not used, otherwise the |
|
// changing of interval is too insensitive. |
|
else { |
|
cache.lastTickCount = tickCount; |
|
cache.lastAutoInterval = interval; |
|
} |
|
|
|
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 !== 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.<number>} |
|
* @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.contain(this.dataToCoord(data)); |
|
}, |
|
|
|
/** |
|
* Get coord extent. |
|
* @return {Array.<number>} |
|
*/ |
|
getExtent: function () { |
|
return this._extent.slice(); |
|
}, |
|
|
|
/** |
|
* Get precision used for formatting |
|
* @param {Array.<number>} [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.<number>} 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 {number} [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.<Object>} [{ |
|
* 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, result.tickCategoryInterval, alignWithLabel, opt.clamp); |
|
return ticksCoords; |
|
}, |
|
|
|
/** |
|
* @return {Array.<Object>} [{ |
|
* 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, tickCategoryInterval, alignWithLabel, clamp) { |
|
var ticksLen = ticksCoords.length; |
|
|
|
if (!axis.onBand || alignWithLabel || !ticksLen) { |
|
return; |
|
} |
|
|
|
var axisExtent = axis.getExtent(); |
|
var last; |
|
|
|
if (ticksLen === 1) { |
|
ticksCoords[0].coord = axisExtent[0]; |
|
last = ticksCoords[1] = { |
|
coord: axisExtent[0] |
|
}; |
|
} else { |
|
var shift = ticksCoords[1].coord - ticksCoords[0].coord; |
|
each$1(ticksCoords, function (ticksItem) { |
|
ticksItem.coord -= shift / 2; |
|
var tickCategoryInterval = tickCategoryInterval || 0; // Avoid split a single data item when odd interval. |
|
|
|
if (tickCategoryInterval % 2 > 0) { |
|
ticksItem.coord -= shift / ((tickCategoryInterval + 1) * 2); |
|
} |
|
}); |
|
last = { |
|
coord: ticksCoords[ticksLen - 1].coord + shift |
|
}; |
|
ticksCoords.push(last); |
|
} |
|
|
|
var inverse = axisExtent[0] > axisExtent[1]; |
|
|
|
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) { |
|
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', '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) { |
|
return createListFromArray(this.getSource(), this); |
|
}, |
|
defaultOption: { |
|
zlevel: 0, |
|
z: 2, |
|
coordinateSystem: 'cartesian2d', |
|
legendHoverLink: true, |
|
hoverAnimation: true, |
|
// stack: null |
|
// xAxisIndex: 0, |
|
// yAxisIndex: 0, |
|
// polarIndex: 0, |
|
// If clip the overflow value |
|
clipOverflow: 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.<number>} [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' : 'pointer'; |
|
}; |
|
/** |
|
* 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.<number>} 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 |
|
}); |
|
} |
|
|
|
var itemStyle = seriesScope && seriesScope.itemStyle; |
|
var hoverItemStyle = seriesScope && seriesScope.hoverItemStyle; |
|
var symbolRotate = seriesScope && seriesScope.symbolRotate; |
|
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(); |
|
symbolRotate = itemModel.getShallow('symbolRotate'); |
|
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; |
|
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.off('mouseover').off('mouseout').off('emphasis').off('normal'); |
|
symbolPath.hoverStyle = hoverItemStyle; // FIXME |
|
// Do not use symbol.trigger('emphasis'), but use symbol.highlight() instead. |
|
|
|
setHoverStyle(symbolPath); |
|
symbolPath.__symbolOriginalScale = getScale(symbolSize); |
|
|
|
if (hoverAnimation && seriesModel.isAnimationEnabled()) { |
|
// Note: consider `off`, should use static function here. |
|
symbolPath.on('mouseover', onMouseOver).on('mouseout', onMouseOut).on('emphasis', onEmphasis).on('normal', onNormal); |
|
} |
|
}; |
|
|
|
function onMouseOver() { |
|
// see comment in `graphic.isInEmphasis` |
|
!isInEmphasis(this) && onEmphasis.call(this); |
|
} |
|
|
|
function onMouseOut() { |
|
// see comment in `graphic.isInEmphasis` |
|
!isInEmphasis(this) && onNormal.call(this); |
|
} |
|
|
|
function onEmphasis() { |
|
// 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; |
|
} |
|
|
|
var scale = this.__symbolOriginalScale; |
|
var ratio = scale[1] / scale[0]; |
|
this.animateTo({ |
|
scale: [Math.max(scale[0] * 1.1, scale[0] + 3), Math.max(scale[1] * 1.1, scale[1] + 3 * ratio)] |
|
}, 400, 'elasticOut'); |
|
} |
|
|
|
function onNormal() { |
|
if (this.incremental || this.useHoverLayer) { |
|
return; |
|
} |
|
|
|
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. |
|
*/ |
|
// '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. |
|
*/ |
|
// 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 getSmooth(smooth) { |
|
return typeof smooth === 'number' ? smooth : smooth ? 0.5 : 0; |
|
} |
|
|
|
function getAxisExtentWithGap(axis) { |
|
var extent = axis.getGlobalExtent(); |
|
|
|
if (axis.onBand) { |
|
// Remove extra 1px to avoid line miter in clipped edge |
|
var halfBandWidth = axis.getBandWidth() / 2 - 1; |
|
var dir = extent[1] > extent[0] ? 1 : -1; |
|
extent[0] += dir * halfBandWidth; |
|
extent[1] -= dir * halfBandWidth; |
|
} |
|
|
|
return extent; |
|
} |
|
/** |
|
* @param {module:echarts/coord/cartesian/Cartesian2D|module:echarts/coord/polar/Polar} coordSys |
|
* @param {module:echarts/data/List} data |
|
* @param {Object} dataCoordInfo |
|
* @param {Array.<Array.<number>>} 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 createGridClipShape(cartesian, hasAnimation, forSymbol, seriesModel) { |
|
var xExtent = getAxisExtentWithGap(cartesian.getAxis('x')); |
|
var yExtent = getAxisExtentWithGap(cartesian.getAxis('y')); |
|
var isHorizontal = cartesian.getBaseAxis().isHorizontal(); |
|
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; // Avoid float number rounding error for symbol on the edge of axis extent. |
|
// See #7913 and `test/dataZoom-clip.html`. |
|
|
|
if (forSymbol) { |
|
x -= 0.5; |
|
width += 0.5; |
|
y -= 0.5; |
|
height += 0.5; |
|
} else { |
|
var lineWidth = seriesModel.get('lineStyle.width') || 2; // Expand clip shape to avoid clipping when line value exceeds axis |
|
|
|
var expandSize = seriesModel.get('clipOverflow') ? lineWidth / 2 : Math.max(width, height); |
|
|
|
if (isHorizontal) { |
|
y -= expandSize; |
|
height += expandSize * 2; |
|
} else { |
|
x -= expandSize; |
|
width += expandSize * 2; |
|
} |
|
} |
|
|
|
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 createPolarClipShape(polar, hasAnimation, forSymbol, seriesModel) { |
|
var angleAxis = polar.getAngleAxis(); |
|
var radiusAxis = polar.getRadiusAxis(); |
|
var radiusExtent = radiusAxis.getExtent().slice(); |
|
radiusExtent[0] > radiusExtent[1] && radiusExtent.reverse(); |
|
var angleExtent = angleAxis.getExtent(); |
|
var RADIAN = Math.PI / 180; // Avoid float number rounding error for symbol on the edge of axis extent. |
|
|
|
if (forSymbol) { |
|
radiusExtent[0] -= 0.5; |
|
radiusExtent[1] += 0.5; |
|
} |
|
|
|
var clipPath = new Sector({ |
|
shape: { |
|
cx: round$2(polar.cx, 1), |
|
cy: round$2(polar.cy, 1), |
|
r0: round$2(radiusExtent[0], 1), |
|
r: round$2(radiusExtent[1], 1), |
|
startAngle: -angleExtent[0] * RADIAN, |
|
endAngle: -angleExtent[1] * RADIAN, |
|
clockwise: angleAxis.inverse |
|
} |
|
}); |
|
|
|
if (hasAnimation) { |
|
clipPath.shape.endAngle = -angleExtent[0] * RADIAN; |
|
initProps(clipPath, { |
|
shape: { |
|
endAngle: -angleExtent[1] * RADIAN |
|
} |
|
}, seriesModel); |
|
} |
|
|
|
return clipPath; |
|
} |
|
|
|
function createClipShape(coordSys, hasAnimation, forSymbol, seriesModel) { |
|
return coordSys.type === 'polar' ? createPolarClipShape(coordSys, hasAnimation, forSymbol, seriesModel) : createGridClipShape(coordSys, hasAnimation, forSymbol, seriesModel); |
|
} |
|
|
|
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') { |
|
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) { |
|
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; |
|
} |
|
|
|
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'); // Initialization animation or coordinate system changed |
|
|
|
if (!(polyline && prevCoordSys.type === coordSys.type && step === this._step)) { |
|
showSymbol && symbolDraw.updateData(data, { |
|
isIgnore: isIgnoreFunc, |
|
clipShape: createClipShape(coordSys, false, true, seriesModel) |
|
}); |
|
|
|
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(createClipShape(coordSys, true, false, 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(createClipShape(coordSys, false, 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: createClipShape(coordSys, false, true, seriesModel) |
|
}); // 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; |
|
} |
|
|
|
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.<Array.<number>>} 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.<Array.<number>>} stackedOnPoints |
|
* @param {Array.<Array.<number>>} 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); |
|
} // `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') || defaultSymbolType; |
|
var symbolSize = seriesModel.get('symbolSize'); |
|
var keepAspect = seriesModel.get('symbolKeepAspect'); |
|
data.setVisual({ |
|
legendSymbol: legendSymbol || symbolType, |
|
symbol: symbolType, |
|
symbolSize: symbolSize, |
|
symbolKeepAspect: keepAspect |
|
}); // Only visible series has each data be visual encoded |
|
|
|
if (ecModel.isSeriesFiltered(seriesModel)) { |
|
return; |
|
} |
|
|
|
var hasCallback = typeof symbolSize === 'function'; |
|
|
|
function dataEach(data, idx) { |
|
if (typeof symbolSize === 'function') { |
|
var rawValue = seriesModel.getRawValue(idx); // FIXME |
|
|
|
var params = seriesModel.getDataParams(idx); |
|
data.setItemVisual(idx, 'symbolSize', symbolSize(rawValue, params)); |
|
} |
|
|
|
if (data.hasItemOption) { |
|
var itemModel = data.getItemModel(idx); |
|
var itemSymbolType = itemModel.getShallow('symbol', true); |
|
var itemSymbolSize = itemModel.getShallow('symbolSize', 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 (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 layoutPoints = 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.<number>}, 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 = 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.<module:echarts/coord/Cartesian~Axis>} |
|
*/ |
|
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.<number>|Object.<string, number>} val |
|
* @return {Array.<number>|Object.<string, number>} |
|
*/ |
|
dataToCoord: function (val) { |
|
return this._dataCoordConvert(val, 'dataToCoord'); |
|
}, |
|
|
|
/** |
|
* Convert coord in nd space to data |
|
* @param {Array.<number>|Object.<string, number>} val |
|
* @return {Array.<number>|Object.<string, number>} |
|
*/ |
|
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.<string>} |
|
* @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.<number>} 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.<number>} data |
|
* @return {boolean} |
|
*/ |
|
containData: function (data) { |
|
return this.getAxis('x').containData(data[0]) && this.getAxis('y').containData(data[1]); |
|
}, |
|
|
|
/** |
|
* @param {Array.<number>} data |
|
* @param {Array.<number>} out |
|
* @return {Array.<number>} |
|
*/ |
|
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.<number>} data |
|
* @param {Array.<number>} out |
|
* @return {Array.<number>} |
|
*/ |
|
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.<number>} point |
|
* @param {Array.<number>} out |
|
* @return {Array.<number>} |
|
*/ |
|
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'); |
|
} |
|
}; |
|
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.<number>} 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 <module:echarts/coord/cartesian/Grid>. |
|
* @return {Array.<module:echarts/coord/cartesian/Axis2D>} |
|
* 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.<number>} |
|
*/ |
|
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 defualt 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 |
|
|
|
}, 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. |
|
*/ |
|
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 |
|
*/ |
|
/** |
|
* 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.<string, module:echarts/coord/cartesian/Cartesian2D>} |
|
* @private |
|
*/ |
|
this._coordsMap = {}; |
|
/** |
|
* @type {Array.<module:echarts/coord/cartesian/Cartesian>} |
|
* @private |
|
*/ |
|
|
|
this._coordsList = []; |
|
/** |
|
* @type {Object.<string, module:echarts/coord/cartesian/Axis2D>} |
|
* @private |
|
*/ |
|
|
|
this._axesMap = {}; |
|
/** |
|
* @type {Array.<module:echarts/coord/cartesian/Axis2D>} |
|
* @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.<module:echarts/coord/Axis>} |
|
*/ |
|
|
|
|
|
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 = 'bottom'; |
|
|
|
if (axisPositionUsed[axisPosition]) { |
|
axisPosition = axisPosition === 'top' ? 'bottom' : 'top'; |
|
} |
|
} |
|
} else { |
|
// Fix position |
|
if (axisPosition !== 'left' && axisPosition !== 'right') { |
|
// Default left of Y |
|
axisPosition = 'left'; |
|
|
|
if (axisPositionUsed[axisPosition]) { |
|
axisPosition = axisPosition === '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]; |
|
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(); |
|
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; |
|
|
|
function makeAxisEventDataBase(axisModel) { |
|
var eventData = { |
|
componentType: axisModel.mainType, |
|
componentIndex: axisModel.componentIndex |
|
}; |
|
eventData[axisModel.mainType + 'Index'] = axisModel.componentIndex; |
|
return eventData; |
|
} |
|
/** |
|
* 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.<number>} 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(subPixelOptimizeLine({ |
|
// Id for animation |
|
anid: 'line', |
|
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 tickEls = buildAxisTick(this, axisModel, opt); |
|
var labelEls = buildAxisLabel(this, axisModel, opt); |
|
fixMinMaxLabelShow(axisModel, labelEls, tickEls); |
|
}, |
|
|
|
/** |
|
* @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: isSilent(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: labelLayout.textAlign, |
|
textVerticalAlign: 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(); |
|
} |
|
}; |
|
/** |
|
* @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 |
|
}; |
|
} |
|
|
|
function isSilent(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 buildAxisTick(axisBuilder, axisModel, opt) { |
|
var axis = axisModel.axis; |
|
|
|
if (!axisModel.get('axisTick.show') || axis.scale.isBlank()) { |
|
return; |
|
} |
|
|
|
var tickModel = axisModel.getModel('axisTick'); |
|
var lineStyleModel = tickModel.getModel('lineStyle'); |
|
var tickLen = tickModel.get('length'); |
|
var ticksCoords = axis.getTicksCoords(); |
|
var pt1 = []; |
|
var pt2 = []; |
|
var matrix = axisBuilder._transform; |
|
var tickEls = []; |
|
|
|
for (var i = 0; i < ticksCoords.length; i++) { |
|
var tickCoord = ticksCoords[i].coord; |
|
pt1[0] = tickCoord; |
|
pt1[1] = 0; |
|
pt2[0] = tickCoord; |
|
pt2[1] = opt.tickDirection * tickLen; |
|
|
|
if (matrix) { |
|
applyTransform(pt1, pt1, matrix); |
|
applyTransform(pt2, pt2, matrix); |
|
} // Tick line, Not use group transform to have better line draw |
|
|
|
|
|
var tickEl = new Line(subPixelOptimizeLine({ |
|
// Id for animation |
|
anid: 'tick_' + ticksCoords[i].tickValue, |
|
shape: { |
|
x1: pt1[0], |
|
y1: pt1[1], |
|
x2: pt2[0], |
|
y2: pt2[1] |
|
}, |
|
style: defaults(lineStyleModel.getLineStyle(), { |
|
stroke: axisModel.get('axisLine.lineStyle.color') |
|
}), |
|
z2: 2, |
|
silent: true |
|
})); |
|
axisBuilder.group.add(tickEl); |
|
tickEls.push(tickEl); |
|
} |
|
|
|
return tickEls; |
|
} |
|
|
|
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(true); |
|
var labelEls = []; |
|
var silent = isSilent(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) { |
|
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. |
|
*/ |
|
var axisBuilderAttrs = ['axisLine', 'axisTickLabel', 'axisName']; |
|
var selfBuilderAttrs = ['splitArea', 'splitLine']; // function getAlignWithLabel(model, axisModel) { |
|
// var alignWithLabel = model.get('alignWithLabel'); |
|
// if (alignWithLabel === 'auto') { |
|
// alignWithLabel = axisModel.get('axisTick.alignWithLabel'); |
|
// } |
|
// return alignWithLabel; |
|
// } |
|
|
|
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 () { |
|
this._splitAreaColors = null; |
|
}, |
|
|
|
/** |
|
* @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 = []; // Simple optimization |
|
// Batching the lines if color are the same |
|
|
|
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(subPixelOptimizeLine({ |
|
anid: tickValue != null ? 'line_' + ticksCoords[i].tickValue : null, |
|
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 |
|
*/ |
|
_splitArea: function (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 = this._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); |
|
|
|
this._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; |
|
} |
|
|
|
this._splitAreaColors = newSplitAreaColors; |
|
} |
|
}); |
|
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. |
|
*/ |
|
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. |
|
*/ |
|
registerVisual(visualSymbol('line', 'circle', 'line')); |
|
registerLayout(layoutPoints('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. |
|
*/ |
|
/** |
|
* [Usage]: |
|
* (1) |
|
* createListSimply(seriesModel, ['value']); |
|
* (2) |
|
* createListSimply(seriesModel, { |
|
* coordDimensions: ['value'], |
|
* dimensionsCount: 5 |
|
* }); |
|
* |
|
* @param {module:echarts/model/Series} seriesModel |
|
* @param {Object|Array.<string|Object>} opt opt or coordDimensions |
|
* The options in opt, see `echarts/data/helper/createDimensions` |
|
* @param {Array.<string>} [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. |
|
*/ |
|
var seriesModelMixin = { |
|
/** |
|
* @private |
|
* @type {string} |
|
*/ |
|
_baseAxisDim: null, |
|
|
|
/** |
|
* @override |
|
*/ |
|
getInitialData: function (option, ecModel) { |
|
// When both types of xAxis and yAxis are 'value', layout is |
|
// needed to be specified by user. Otherwise, layout can be |
|
// judged by which axis is category. |
|
var ordinalMeta; |
|
var xAxisModel = ecModel.getComponent('xAxis', this.get('xAxisIndex')); |
|
var yAxisModel = ecModel.getComponent('yAxis', this.get('yAxisIndex')); |
|
var xAxisType = xAxisModel.get('type'); |
|
var yAxisType = yAxisModel.get('type'); |
|
var addOrdinal; // FIXME |
|
// 考虑时间轴 |
|
|
|
if (xAxisType === 'category') { |
|
option.layout = 'horizontal'; |
|
ordinalMeta = xAxisModel.getOrdinalMeta(); |
|
addOrdinal = true; |
|
} else if (yAxisType === 'category') { |
|
option.layout = 'vertical'; |
|
ordinalMeta = yAxisModel.getOrdinalMeta(); |
|
addOrdinal = true; |
|
} else { |
|
option.layout = option.layout || 'horizontal'; |
|
} |
|
|
|
var coordDims = ['x', 'y']; |
|
var baseAxisDimIndex = option.layout === 'horizontal' ? 0 : 1; |
|
var baseAxisDim = this._baseAxisDim = coordDims[baseAxisDimIndex]; |
|
var otherAxisDim = coordDims[1 - baseAxisDimIndex]; |
|
var axisModels = [xAxisModel, yAxisModel]; |
|
var baseAxisType = axisModels[baseAxisDimIndex].get('type'); |
|
var otherAxisType = axisModels[1 - baseAxisDimIndex].get('type'); |
|
var data = option.data; // ??? FIXME make a stage to perform data transfrom. |
|
// MUST create a new data, consider setOption({}) again. |
|
|
|
if (data && addOrdinal) { |
|
var newOptionData = []; |
|
each$1(data, function (item, index) { |
|
var newItem; |
|
|
|
if (item.value && isArray(item.value)) { |
|
newItem = item.value.slice(); |
|
item.value.unshift(index); |
|
} else if (isArray(item)) { |
|
newItem = item.slice(); |
|
item.unshift(index); |
|
} else { |
|
newItem = item; |
|
} |
|
|
|
newOptionData.push(newItem); |
|
}); |
|
option.data = newOptionData; |
|
} |
|
|
|
var defaultValueDimensions = this.defaultValueDimensions; |
|
return createListSimply(this, { |
|
coordDimensions: [{ |
|
name: baseAxisDim, |
|
type: getDimensionTypeByAxis(baseAxisType), |
|
ordinalMeta: ordinalMeta, |
|
otherDims: { |
|
tooltip: false, |
|
itemName: 0 |
|
}, |
|
dimsDef: ['base'] |
|
}, { |
|
name: otherAxisDim, |
|
type: getDimensionTypeByAxis(otherAxisType), |
|
dimsDef: defaultValueDimensions.slice() |
|
}], |
|
dimensionsCount: defaultValueDimensions.length + 1 |
|
}); |
|
}, |
|
|
|
/** |
|
* If horizontal, base axis is x, otherwise y. |
|
* @override |
|
*/ |
|
getBaseAxis: function () { |
|
var dim = this._baseAxisDim; |
|
return this.ecModel.getComponent(dim + 'Axis', this.get(dim + 'AxisIndex')).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 BoxplotSeries = SeriesModel.extend({ |
|
type: 'series.boxplot', |
|
dependencies: ['xAxis', 'yAxis', 'grid'], |
|
// TODO |
|
// box width represents group size, so dimension should have 'size'. |
|
|
|
/** |
|
* @see <https://en.wikipedia.org/wiki/Box_plot> |
|
* The meanings of 'min' and 'max' depend on user, |
|
* and echarts do not need to know it. |
|
* @readOnly |
|
*/ |
|
defaultValueDimensions: [{ |
|
name: 'min', |
|
defaultTooltip: true |
|
}, { |
|
name: 'Q1', |
|
defaultTooltip: true |
|
}, { |
|
name: 'median', |
|
defaultTooltip: true |
|
}, { |
|
name: 'Q3', |
|
defaultTooltip: true |
|
}, { |
|
name: 'max', |
|
defaultTooltip: true |
|
}], |
|
|
|
/** |
|
* @type {Array.<string>} |
|
* @readOnly |
|
*/ |
|
dimensions: null, |
|
|
|
/** |
|
* @override |
|
*/ |
|
defaultOption: { |
|
zlevel: 0, |
|
// 一级层叠 |
|
z: 2, |
|
// 二级层叠 |
|
coordinateSystem: 'cartesian2d', |
|
legendHoverLink: true, |
|
hoverAnimation: true, |
|
// xAxisIndex: 0, |
|
// yAxisIndex: 0, |
|
layout: null, |
|
// 'horizontal' or 'vertical' |
|
boxWidth: [7, 50], |
|
// [min, max] can be percent of band width. |
|
itemStyle: { |
|
color: '#fff', |
|
borderWidth: 1 |
|
}, |
|
emphasis: { |
|
itemStyle: { |
|
borderWidth: 2, |
|
shadowBlur: 5, |
|
shadowOffsetX: 2, |
|
shadowOffsetY: 2, |
|
shadowColor: 'rgba(0,0,0,0.4)' |
|
} |
|
}, |
|
animationEasing: 'elasticOut', |
|
animationDuration: 800 |
|
} |
|
}); |
|
mixin(BoxplotSeries, seriesModelMixin, 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 NORMAL_ITEM_STYLE_PATH = ['itemStyle']; |
|
var EMPHASIS_ITEM_STYLE_PATH = ['emphasis', 'itemStyle']; |
|
var BoxplotView = Chart.extend({ |
|
type: 'boxplot', |
|
render: function (seriesModel, ecModel, api) { |
|
var data = seriesModel.getData(); |
|
var group = this.group; |
|
var oldData = this._data; // There is no old data only when first rendering or switching from |
|
// stream mode to normal mode, where previous elements should be removed. |
|
|
|
if (!this._data) { |
|
group.removeAll(); |
|
} |
|
|
|
var constDim = seriesModel.get('layout') === 'horizontal' ? 1 : 0; |
|
data.diff(oldData).add(function (newIdx) { |
|
if (data.hasValue(newIdx)) { |
|
var itemLayout = data.getItemLayout(newIdx); |
|
var symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, true); |
|
data.setItemGraphicEl(newIdx, symbolEl); |
|
group.add(symbolEl); |
|
} |
|
}).update(function (newIdx, oldIdx) { |
|
var symbolEl = oldData.getItemGraphicEl(oldIdx); // Empty data |
|
|
|
if (!data.hasValue(newIdx)) { |
|
group.remove(symbolEl); |
|
return; |
|
} |
|
|
|
var itemLayout = data.getItemLayout(newIdx); |
|
|
|
if (!symbolEl) { |
|
symbolEl = createNormalBox(itemLayout, data, newIdx, constDim); |
|
} else { |
|
updateNormalBoxData(itemLayout, symbolEl, data, newIdx); |
|
} |
|
|
|
group.add(symbolEl); |
|
data.setItemGraphicEl(newIdx, symbolEl); |
|
}).remove(function (oldIdx) { |
|
var el = oldData.getItemGraphicEl(oldIdx); |
|
el && group.remove(el); |
|
}).execute(); |
|
this._data = data; |
|
}, |
|
remove: function (ecModel) { |
|
var group = this.group; |
|
var data = this._data; |
|
this._data = null; |
|
data && data.eachItemGraphicEl(function (el) { |
|
el && group.remove(el); |
|
}); |
|
}, |
|
dispose: noop |
|
}); |
|
var BoxPath = Path.extend({ |
|
type: 'boxplotBoxPath', |
|
shape: {}, |
|
buildPath: function (ctx, shape) { |
|
var ends = shape.points; |
|
var i = 0; |
|
ctx.moveTo(ends[i][0], ends[i][1]); |
|
i++; |
|
|
|
for (; i < 4; i++) { |
|
ctx.lineTo(ends[i][0], ends[i][1]); |
|
} |
|
|
|
ctx.closePath(); |
|
|
|
for (; i < ends.length; i++) { |
|
ctx.moveTo(ends[i][0], ends[i][1]); |
|
i++; |
|
ctx.lineTo(ends[i][0], ends[i][1]); |
|
} |
|
} |
|
}); |
|
|
|
function createNormalBox(itemLayout, data, dataIndex, constDim, isInit) { |
|
var ends = itemLayout.ends; |
|
var el = new BoxPath({ |
|
shape: { |
|
points: isInit ? transInit(ends, constDim, itemLayout) : ends |
|
} |
|
}); |
|
updateNormalBoxData(itemLayout, el, data, dataIndex, isInit); |
|
return el; |
|
} |
|
|
|
function updateNormalBoxData(itemLayout, el, data, dataIndex, isInit) { |
|
var seriesModel = data.hostModel; |
|
var updateMethod = graphic[isInit ? 'initProps' : 'updateProps']; |
|
updateMethod(el, { |
|
shape: { |
|
points: itemLayout.ends |
|
} |
|
}, seriesModel, dataIndex); |
|
var itemModel = data.getItemModel(dataIndex); |
|
var normalItemStyleModel = itemModel.getModel(NORMAL_ITEM_STYLE_PATH); |
|
var borderColor = data.getItemVisual(dataIndex, 'color'); // Exclude borderColor. |
|
|
|
var itemStyle = normalItemStyleModel.getItemStyle(['borderColor']); |
|
itemStyle.stroke = borderColor; |
|
itemStyle.strokeNoScale = true; |
|
el.useStyle(itemStyle); |
|
el.z2 = 100; |
|
var hoverStyle = itemModel.getModel(EMPHASIS_ITEM_STYLE_PATH).getItemStyle(); |
|
setHoverStyle(el, hoverStyle); |
|
} |
|
|
|
function transInit(points, dim, itemLayout) { |
|
return map(points, function (point) { |
|
point = point.slice(); |
|
point[dim] = itemLayout.initBaseline; |
|
return point; |
|
}); |
|
} |
|
|
|
/* |
|
* 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 borderColorQuery = ['itemStyle', 'borderColor']; |
|
var boxplotVisual = function (ecModel, api) { |
|
var globalColors = ecModel.get('color'); |
|
ecModel.eachRawSeriesByType('boxplot', function (seriesModel) { |
|
var defaulColor = globalColors[seriesModel.seriesIndex % globalColors.length]; |
|
var data = seriesModel.getData(); |
|
data.setVisual({ |
|
legendSymbol: 'roundRect', |
|
// Use name 'color' but not 'borderColor' for legend usage and |
|
// visual coding from other component like dataRange. |
|
color: seriesModel.get(borderColorQuery) || defaulColor |
|
}); // Only visible series has each data be visual encoded |
|
|
|
if (!ecModel.isSeriesFiltered(seriesModel)) { |
|
data.each(function (idx) { |
|
var itemModel = data.getItemModel(idx); |
|
data.setItemVisual(idx, { |
|
color: itemModel.get(borderColorQuery, 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 each$7 = each$1; |
|
var boxplotLayout = function (ecModel) { |
|
var groupResult = groupSeriesByAxis(ecModel); |
|
each$7(groupResult, function (groupItem) { |
|
var seriesModels = groupItem.seriesModels; |
|
|
|
if (!seriesModels.length) { |
|
return; |
|
} |
|
|
|
calculateBase(groupItem); |
|
each$7(seriesModels, function (seriesModel, idx) { |
|
layoutSingleSeries(seriesModel, groupItem.boxOffsetList[idx], groupItem.boxWidthList[idx]); |
|
}); |
|
}); |
|
}; |
|
/** |
|
* Group series by axis. |
|
*/ |
|
|
|
function groupSeriesByAxis(ecModel) { |
|
var result = []; |
|
var axisList = []; |
|
ecModel.eachSeriesByType('boxplot', function (seriesModel) { |
|
var baseAxis = seriesModel.getBaseAxis(); |
|
var idx = indexOf(axisList, baseAxis); |
|
|
|
if (idx < 0) { |
|
idx = axisList.length; |
|
axisList[idx] = baseAxis; |
|
result[idx] = { |
|
axis: baseAxis, |
|
seriesModels: [] |
|
}; |
|
} |
|
|
|
result[idx].seriesModels.push(seriesModel); |
|
}); |
|
return result; |
|
} |
|
/** |
|
* Calculate offset and box width for each series. |
|
*/ |
|
|
|
|
|
function calculateBase(groupItem) { |
|
var extent; |
|
var baseAxis = groupItem.axis; |
|
var seriesModels = groupItem.seriesModels; |
|
var seriesCount = seriesModels.length; |
|
var boxWidthList = groupItem.boxWidthList = []; |
|
var boxOffsetList = groupItem.boxOffsetList = []; |
|
var boundList = []; |
|
var bandWidth; |
|
|
|
if (baseAxis.type === 'category') { |
|
bandWidth = baseAxis.getBandWidth(); |
|
} else { |
|
var maxDataCount = 0; |
|
each$7(seriesModels, function (seriesModel) { |
|
maxDataCount = Math.max(maxDataCount, seriesModel.getData().count()); |
|
}); |
|
extent = baseAxis.getExtent(), Math.abs(extent[1] - extent[0]) / maxDataCount; |
|
} |
|
|
|
each$7(seriesModels, function (seriesModel) { |
|
var boxWidthBound = seriesModel.get('boxWidth'); |
|
|
|
if (!isArray(boxWidthBound)) { |
|
boxWidthBound = [boxWidthBound, boxWidthBound]; |
|
} |
|
|
|
boundList.push([parsePercent$1(boxWidthBound[0], bandWidth) || 0, parsePercent$1(boxWidthBound[1], bandWidth) || 0]); |
|
}); |
|
var availableWidth = bandWidth * 0.8 - 2; |
|
var boxGap = availableWidth / seriesCount * 0.3; |
|
var boxWidth = (availableWidth - boxGap * (seriesCount - 1)) / seriesCount; |
|
var base = boxWidth / 2 - availableWidth / 2; |
|
each$7(seriesModels, function (seriesModel, idx) { |
|
boxOffsetList.push(base); |
|
base += boxGap + boxWidth; |
|
boxWidthList.push(Math.min(Math.max(boxWidth, boundList[idx][0]), boundList[idx][1])); |
|
}); |
|
} |
|
/** |
|
* Calculate points location for each series. |
|
*/ |
|
|
|
|
|
function layoutSingleSeries(seriesModel, offset, boxWidth) { |
|
var coordSys = seriesModel.coordinateSystem; |
|
var data = seriesModel.getData(); |
|
var halfWidth = boxWidth / 2; |
|
var cDimIdx = seriesModel.get('layout') === 'horizontal' ? 0 : 1; |
|
var vDimIdx = 1 - cDimIdx; |
|
var coordDims = ['x', 'y']; |
|
var cDim = data.mapDimension(coordDims[cDimIdx]); |
|
var vDims = data.mapDimension(coordDims[vDimIdx], true); |
|
|
|
if (cDim == null || vDims.length < 5) { |
|
return; |
|
} |
|
|
|
for (var dataIndex = 0; dataIndex < data.count(); dataIndex++) { |
|
var axisDimVal = data.get(cDim, dataIndex); |
|
var median = getPoint(axisDimVal, vDims[2], dataIndex); |
|
var end1 = getPoint(axisDimVal, vDims[0], dataIndex); |
|
var end2 = getPoint(axisDimVal, vDims[1], dataIndex); |
|
var end4 = getPoint(axisDimVal, vDims[3], dataIndex); |
|
var end5 = getPoint(axisDimVal, vDims[4], dataIndex); |
|
var ends = []; |
|
addBodyEnd(ends, end2, 0); |
|
addBodyEnd(ends, end4, 1); |
|
ends.push(end1, end2, end5, end4); |
|
layEndLine(ends, end1); |
|
layEndLine(ends, end5); |
|
layEndLine(ends, median); |
|
data.setItemLayout(dataIndex, { |
|
initBaseline: median[vDimIdx], |
|
ends: ends |
|
}); |
|
} |
|
|
|
function getPoint(axisDimVal, dimIdx, dataIndex) { |
|
var val = data.get(dimIdx, dataIndex); |
|
var p = []; |
|
p[cDimIdx] = axisDimVal; |
|
p[vDimIdx] = val; |
|
var point; |
|
|
|
if (isNaN(axisDimVal) || isNaN(val)) { |
|
point = [NaN, NaN]; |
|
} else { |
|
point = coordSys.dataToPoint(p); |
|
point[cDimIdx] += offset; |
|
} |
|
|
|
return point; |
|
} |
|
|
|
function addBodyEnd(ends, point, start) { |
|
var point1 = point.slice(); |
|
var point2 = point.slice(); |
|
point1[cDimIdx] += halfWidth; |
|
point2[cDimIdx] -= halfWidth; |
|
start ? ends.push(point1, point2) : ends.push(point2, point1); |
|
} |
|
|
|
function layEndLine(ends, endCenter) { |
|
var from = endCenter.slice(); |
|
var to = endCenter.slice(); |
|
from[cDimIdx] -= halfWidth; |
|
to[cDimIdx] += halfWidth; |
|
ends.push(from, to); |
|
} |
|
} |
|
|
|
/* |
|
* 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. |
|
*/ |
|
registerVisual(boxplotVisual); |
|
registerLayout(boxplotLayout); |
|
|
|
/* |
|
* 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 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 || {}; |
|
}, |
|
mergeOption: function (option) { |
|
LegendModel.superCall(this, 'mergeOption', option); |
|
}, |
|
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.legendDataProvider) { |
|
var data = seriesModel.legendDataProvider(); |
|
var names = data.mapArray(data.getName); |
|
|
|
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.<string>} |
|
* @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.<module:echarts/model/Model>} |
|
* @private |
|
*/ |
|
|
|
this._data = legendData; |
|
}, |
|
|
|
/** |
|
* @return {Array.<module:echarts/model/Model>} |
|
*/ |
|
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); |
|
}, |
|
|
|
/** |
|
* @param {string} name |
|
*/ |
|
isSelected: function (name) { |
|
var selected = this.option.selected; |
|
return !(selected.hasOwnProperty(name) && !selected[name]) && indexOf(this._availableNames, name) >= 0; |
|
}, |
|
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, |
|
// 图例图形宽度 |
|
itemWidth: 25, |
|
// 图例图形高度 |
|
itemHeight: 14, |
|
// 图例关闭时候的颜色 |
|
inactiveColor: '#ccc', |
|
textStyle: { |
|
// 图例文字颜色 |
|
color: '#333' |
|
}, |
|
// formatter: '', |
|
// 选择模式,默认开启图例开关 |
|
selectedMode: true, |
|
// 配置默认选中状态,可配合LEGEND.SELECTED事件做动态数据载入 |
|
// selected: null, |
|
// 图例内容(详见legend.data,数组中每一项代表一个item |
|
// data: [], |
|
// 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 { |
|
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 { |
|
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')); |
|
/** |
|
* @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 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$2 = curry; |
|
var each$8 = 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; |
|
/** |
|
* 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; |
|
}, |
|
|
|
/** |
|
* @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'); |
|
|
|
if (!itemAlign || itemAlign === 'auto') { |
|
itemAlign = legendModel.get('left') === 'right' && legendModel.get('orient') === 'vertical' ? 'right' : 'left'; |
|
} |
|
|
|
this.renderInner(itemAlign, legendModel, ecModel, api); // 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); // 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); |
|
}, |
|
|
|
/** |
|
* @protected |
|
*/ |
|
renderInner: function (itemAlign, legendModel, ecModel, api) { |
|
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$8(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; |
|
} // Series legend |
|
|
|
|
|
if (seriesModel) { |
|
var data = seriesModel.getData(); |
|
var color = data.getVisual('color'); // If color is a callback function |
|
|
|
if (typeof color === 'function') { |
|
// Use the first data |
|
color = color(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, selectMode); |
|
|
|
itemGroup.on('click', curry$2(dispatchSelectAction, name, api)).on('mouseover', curry$2(dispatchHighlightAction, seriesModel.name, null, api, excludeSeriesId)).on('mouseout', curry$2(dispatchDownplayAction, seriesModel.name, null, api, excludeSeriesId)); |
|
legendDrawnMap.set(name, true); |
|
} else { |
|
// Data legend of pie, funnel |
|
ecModel.eachRawSeries(function (seriesModel) { |
|
// In case multiple series has same data name |
|
if (legendDrawnMap.get(name)) { |
|
return; |
|
} |
|
|
|
if (seriesModel.legendDataProvider) { |
|
var data = seriesModel.legendDataProvider(); |
|
var idx = data.indexOfName(name); |
|
|
|
if (idx < 0) { |
|
return; |
|
} |
|
|
|
var color = data.getItemVisual(idx, 'color'); |
|
var legendSymbolType = 'roundRect'; |
|
|
|
var itemGroup = this._createItem(name, dataIndex, itemModel, legendModel, legendSymbolType, null, itemAlign, color, selectMode); // FIXME: consider different series has items with the same name. |
|
|
|
|
|
itemGroup.on('click', curry$2(dispatchSelectAction, name, api)) // Should not specify the series name, consider legend controls |
|
// more than one pie series. |
|
.on('mouseover', curry$2(dispatchHighlightAction, null, name, api, excludeSeriesId)).on('mouseout', curry$2(dispatchDownplayAction, null, name, api, excludeSeriesId)); |
|
legendDrawnMap.set(name, true); |
|
} |
|
}, this); |
|
} |
|
}, this); |
|
}, |
|
_createItem: function (name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, selectMode) { |
|
var itemWidth = legendModel.get('itemWidth'); |
|
var itemHeight = legendModel.get('itemHeight'); |
|
var inactiveColor = legendModel.get('inactiveColor'); |
|
var symbolKeepAspect = legendModel.get('symbolKeepAspect'); |
|
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; |
|
itemGroup.add(createSymbol(legendSymbolType, 0, 0, itemWidth, itemHeight, isSelected ? color : inactiveColor, // symbolKeepAspect default true for legend |
|
symbolKeepAspect == null ? true : symbolKeepAspect)); // 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'; |
|
} // Put symbol in the center |
|
|
|
|
|
itemGroup.add(createSymbol(symbolType, (itemWidth - size) / 2, (itemHeight - size) / 2, size, size, isSelected ? color : inactiveColor, // symbolKeepAspect default true for legend |
|
symbolKeepAspect == null ? true : symbolKeepAspect)); |
|
} |
|
|
|
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) { |
|
var contentGroup = this.getContentGroup(); // Place items in contentGroup. |
|
|
|
box(legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), maxSize.width, maxSize.height); |
|
var contentRect = contentGroup.getBoundingRect(); |
|
contentGroup.attr('position', [-contentRect.x, -contentRect.y]); |
|
return this.group.getBoundingRect(); |
|
}, |
|
|
|
/** |
|
* @protected |
|
*/ |
|
remove: function () { |
|
this.getContentGroup().removeAll(); |
|
this._isFirstRender = true; |
|
} |
|
}); |
|
|
|
function dispatchSelectAction(name, api) { |
|
api.dispatchAction({ |
|
type: 'legendToggleSelect', |
|
name: name |
|
}); |
|
} |
|
|
|
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. |
|
registerProcessor(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); |
|
}, |
|
getOrient: function () { |
|
return this.get('orient') === 'vertical' ? { |
|
index: 1, |
|
name: 'vertical' |
|
} : { |
|
index: 0, |
|
name: 'horizontal' |
|
}; |
|
} |
|
}); // 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) { |
|
var me = this; // Render content items. |
|
|
|
ScrollableLegendView.superCall(this, 'renderInner', itemAlign, legendModel, ecModel, api); |
|
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) { |
|
var contentGroup = this.getContentGroup(); |
|
var containerGroup = this._containerGroup; |
|
var controllerGroup = this._controllerGroup; |
|
var orientIdx = legendModel.getOrient().index; |
|
var wh = WH[orientIdx]; |
|
var hw = WH[1 - orientIdx]; |
|
var yx = XY[1 - orientIdx]; // 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 = this.group.getBoundingRect(); |
|
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 place holders. |
|
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 efffect. |
|
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.<number>, 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 next page. |
|
* pageNextDataIndex: number, null when no previous 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) { |
|
var index; |
|
var contentGroup = this.getContentGroup(); |
|
|
|
if (this._showController) { |
|
contentGroup.eachChild(function (child, idx) { |
|
if (child.__legendDataIndex === targetDataIndex) { |
|
index = idx; |
|
} |
|
}); |
|
} else { |
|
index = 0; |
|
} |
|
|
|
return index; |
|
} |
|
}); |
|
|
|
/* |
|
* 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. |
|
*/ |
|
/** |
|
* @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$9 = each$1; |
|
var curry$3 = 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.<number>} [payload.x] x and y, which are mandatory, specify a point to |
|
* trigger axisPointer and tooltip. |
|
* @param {Array.<number>} [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.<number>|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$3(showPointer, showValueMap), |
|
showTooltip: curry$3(showTooltip, dataByCoordSys) |
|
}; // Process for triggered axes. |
|
|
|
each$9(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) { |
|
// If a point given, it must be contained by the coordinate system. |
|
var coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point); |
|
each$9(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$9(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$9(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$9(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 defualt 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$9(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$9(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$9(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$9(axesInfo, function (axisInfo, key) { |
|
var option = axisInfo.axisPointerModel.option; |
|
option.status === 'show' && each$9(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$10 = each$1; |
|
/** |
|
* @param {string} key |
|
* @param {module:echarts/ExtensionAPI} api |
|
* @param {Function} handler |
|
* param: {string} currTrigger |
|
* param: {Array.<number>} 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$10(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) { |
|
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.<number>} 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.<number>} xy |
|
* @param {Array.<number>} 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', |
|
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.<Object>} 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), |
|
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.<number>} p1 |
|
* @param {Array.<number>} 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.<number>} xy |
|
* @param {Array.<number>} 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, elStyle); |
|
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, elStyle) { |
|
var targetShape = makeLineShape([pixelValue, otherExtent[0]], [pixelValue, otherExtent[1]], getAxisDimIndex(axis)); |
|
subPixelOptimizeLine({ |
|
shape: targetShape, |
|
style: elStyle |
|
}); |
|
return { |
|
type: 'Line', |
|
shape: targetShape |
|
}; |
|
}, |
|
shadow: function (axis, pixelValue, otherExtent, elStyle) { |
|
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. |
|
*/ |
|
// 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$12 = 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()); |
|
fontSize && cssText.push('line-height:' + Math.round(fontSize * 3 / 2) + 'px'); |
|
each$12(['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$12(['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(';') + ';'; |
|
} |
|
/** |
|
* @alias module:echarts/component/tooltip/TooltipContent |
|
* @constructor |
|
*/ |
|
|
|
|
|
function TooltipContent(container, api) { |
|
if (env$1.wxa) { |
|
return null; |
|
} |
|
|
|
var el = document.createElement('div'); |
|
var zr = this._zr = api.getZr(); |
|
this.el = el; |
|
this._x = api.getWidth() / 2; |
|
this._y = api.getHeight() / 2; |
|
container.appendChild(el); |
|
this._container = container; |
|
this._show = false; |
|
/** |
|
* @private |
|
*/ |
|
|
|
this._hideTimeout; |
|
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) { |
|
// Try trigger zrender event to avoid mouse |
|
// in and out shape too frequently |
|
var handler = zr.handler; |
|
normalizeEvent(container, 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 () { |
|
// 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'; |
|
} // Hide the tooltip |
|
// PENDING |
|
// this.hide(); |
|
|
|
}, |
|
show: function (tooltipModel) { |
|
clearTimeout(this._hideTimeout); |
|
var el = this.el; |
|
el.style.cssText = gCssText + assembleCssText(tooltipModel) // http://stackoverflow.com/questions/21125587/css3-transition-not-working-in-chrome-anymore |
|
+ ';left:' + this._x + 'px;top:' + this._y + 'px;' + (tooltipModel.get('extraCssText') || ''); |
|
el.style.display = el.innerHTML ? 'block' : 'none'; // If mouse occsionally move over the tooltip, a mouseout event will be |
|
// triggered by canvas, and cuase some unexpectable result like dragging |
|
// stop, "unfocusAdjacency". Here `pointer-events: none` is used to solve |
|
// it. Although it is not suppored 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 (x, y) { |
|
// 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 zr = this._zr; |
|
var viewportRootOffset; |
|
|
|
if (zr && zr.painter && (viewportRootOffset = zr.painter.getViewportRootOffset())) { |
|
x += viewportRootOffset.offsetLeft; |
|
y += viewportRootOffset.offsetTop; |
|
} |
|
|
|
var style = this.el.style; |
|
style.left = x + 'px'; |
|
style.top = y + 'px'; |
|
this._x = x; |
|
this._y = y; |
|
}, |
|
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 mutiple times |
|
|
|
this._show = false; |
|
this._hideTimeout = setTimeout(bind(this.hide, this), time); |
|
} else { |
|
this.hide(); |
|
} |
|
} |
|
}, |
|
isShow: function () { |
|
return this._show; |
|
}, |
|
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.paddingLeft, 10) + parseInt(stl.paddingRight, 10) + parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10); |
|
height += parseInt(stl.paddingTop, 10) + parseInt(stl.paddingBottom, 10) + 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. |
|
*/ |
|
/** |
|
* @alias module:echarts/component/tooltip/TooltipRichContent |
|
* @constructor |
|
*/ |
|
|
|
function TooltipRichContent(api) { |
|
this._zr = api.getZr(); |
|
this._show = false; |
|
/** |
|
* @private |
|
*/ |
|
|
|
this._hideTimeout; |
|
} |
|
|
|
TooltipRichContent.prototype = { |
|
constructor: TooltipRichContent, |
|
|
|
/** |
|
* @private |
|
* @type {boolean} |
|
*/ |
|
_enterable: true, |
|
|
|
/** |
|
* Update when tooltip is rendered |
|
*/ |
|
update: function () {// noop |
|
}, |
|
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'); |
|
} |
|
|
|
this.el = new Text({ |
|
style: { |
|
rich: markers, |
|
text: content, |
|
textLineHeight: 20, |
|
textBackgroundColor: tooltipModel.get('backgroundColor'), |
|
textBorderRadius: tooltipModel.get('borderRadius'), |
|
textFill: tooltipModel.get('textStyle.color'), |
|
textPadding: tooltipModel.get('padding') |
|
}, |
|
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) { |
|
this.el.attr('position', [x, y]); |
|
} |
|
}, |
|
hide: function () { |
|
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 mutiple times |
|
|
|
this._show = false; |
|
this._hideTimeout = setTimeout(bind(this.hide, this), time); |
|
} else { |
|
this.hide(); |
|
} |
|
} |
|
}, |
|
isShow: function () { |
|
return this._show; |
|
}, |
|
getOuterSize: function () { |
|
return this.getSize(); |
|
} |
|
}; |
|
|
|
/* |
|
* 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$11 = 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); |
|
this._newLine = '<br/>'; |
|
} 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.<Array.<Object>>} |
|
*/ |
|
|
|
this._lastDataByCoordSys = null; |
|
/** |
|
* @private |
|
* @type {boolean} |
|
*/ |
|
|
|
this._alwaysShowContent = tooltipModel.get('alwaysShowContent'); |
|
var tooltipContent = this._tooltipContent; |
|
tooltipContent.update(); |
|
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 |
|
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, |
|
event: {}, |
|
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, |
|
event: {} |
|
}, 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, |
|
event: {} |
|
}, 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 easyer 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$11(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$11(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) shold 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(); |
|
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 cooridinate 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.<number>|Object} positionExpr |
|
* @param {number} x Mouse x |
|
* @param {number} y Mouse y |
|
* @param {boolean} confine Whether confine tooltip content in view rect. |
|
* @param {Object|<Array.<Object>} params |
|
* @param {module:zrender/Element} el target element |
|
* @param {module:echarts/ExtensionAPI} api |
|
* @return {Array.<number>} |
|
*/ |
|
_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$11(lastCoordSys, function (lastItemCoordSys, indexCoordSys) { |
|
var lastDataByAxis = lastItemCoordSys.dataByAxis || {}; |
|
var thisItemCoordSys = dataByCoordSys[indexCoordSys] || {}; |
|
var thisDataByAxis = thisItemCoordSys.dataByAxis || []; |
|
contentNotChanged &= lastDataByAxis.length === thisDataByAxis.length; |
|
contentNotChanged && each$11(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$11(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.hide(); |
|
|
|
unregister('itemTooltip', api); |
|
} |
|
}); |
|
/** |
|
* @param {Array.<Object|module:echarts/model/Model>} 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 () {}); |
|
|
|
exports.version = version; |
|
exports.dependencies = dependencies; |
|
exports.PRIORITY = PRIORITY; |
|
exports.init = init; |
|
exports.connect = connect; |
|
exports.disConnect = disConnect; |
|
exports.disconnect = disconnect; |
|
exports.dispose = dispose; |
|
exports.getInstanceByDom = getInstanceByDom; |
|
exports.getInstanceById = getInstanceById; |
|
exports.registerTheme = registerTheme; |
|
exports.registerPreprocessor = registerPreprocessor; |
|
exports.registerProcessor = registerProcessor; |
|
exports.registerPostUpdate = registerPostUpdate; |
|
exports.registerAction = registerAction; |
|
exports.registerCoordinateSystem = registerCoordinateSystem; |
|
exports.getCoordinateSystemDimensions = getCoordinateSystemDimensions; |
|
exports.registerLayout = registerLayout; |
|
exports.registerVisual = registerVisual; |
|
exports.registerLoading = registerLoading; |
|
exports.extendComponentModel = extendComponentModel; |
|
exports.extendComponentView = extendComponentView; |
|
exports.extendSeriesModel = extendSeriesModel; |
|
exports.extendChartView = extendChartView; |
|
exports.setCanvasCreator = setCanvasCreator; |
|
exports.registerMap = registerMap; |
|
exports.getMap = getMap; |
|
exports.dataTool = dataTool; |
|
exports.zrender = zrender; |
|
exports.number = number; |
|
exports.format = format; |
|
exports.throttle = throttle; |
|
exports.helper = helper; |
|
exports.matrix = matrix; |
|
exports.vector = vector; |
|
exports.color = color; |
|
exports.parseGeoJSON = parseGeoJSON; |
|
exports.parseGeoJson = parseGeoJson; |
|
exports.util = ecUtil; |
|
exports.graphic = graphic$1; |
|
exports.List = List; |
|
exports.Model = Model; |
|
exports.Axis = Axis; |
|
exports.env = env$1; |
|
|
|
})));
|