/*! * ECharts, a javascript interactive chart library. * * Copyright (c) 2015, Baidu Inc. * All rights reserved. * * LICENSE * https://github.com/ecomfe/echarts/blob/master/LICENSE.txt */ /** * @module echarts */ define(function (require) { var GlobalModel = require('./model/Global'); var ExtensionAPI = require('./ExtensionAPI'); var CoordinateSystemManager = require('./CoordinateSystem'); var ComponentModel = require('./model/Component'); var SeriesModel = require('./model/Series'); var ComponentView = require('./view/Component'); var ChartView = require('./view/Chart'); var zrender = require('zrender'); var zrUtil = require('zrender/core/util'); var colorTool = require('zrender/tool/color'); var env = require('zrender/core/env'); var Eventful = require('zrender/mixin/Eventful'); var each = zrUtil.each; var VISUAL_CODING_STAGES = ['echarts', 'chart', 'component']; // TODO Transform first or filter first var PROCESSOR_STAGES = ['transform', 'filter', 'statistic']; /** * @module echarts~MessageCenter */ function MessageCenter() { Eventful.call(this); } zrUtil.mixin(MessageCenter, Eventful); /** * @module echarts~ECharts */ function ECharts (dom, theme, opts) { opts = opts || {}; if (theme) { each(optionPreprocessorFuncs, function (preProcess) { preProcess(theme); }); } /** * @type {string} */ this.id; /** * Group id * @type {string} */ this.group; /** * @type {HTMLDomElement} * @private */ this._dom = dom; /** * @type {module:zrender/ZRender} * @private */ this._zr = zrender.init(dom, { renderer: opts.renderer || 'canvas', devicePixelRatio: opts.devicePixelRatio }); /** * @type {Object} * @private */ this._theme = zrUtil.clone(theme, true); /** * @type {Array.} * @private */ this._chartsList = []; /** * @type {Object.} * @private */ this._chartsMap = {}; /** * @type {Array.} * @private */ this._componentsList = []; /** * @type {Object.} * @private */ this._componentsMap = {}; /** * @type {module:echarts/ExtensionAPI} * @private */ this._api = new ExtensionAPI(this); /** * @type {module:echarts/CoordinateSystem} * @private */ this._coordinateSystem = new CoordinateSystemManager(); Eventful.call(this); /** * @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 = zrUtil.bind(this.resize, this); } var echartsProto = ECharts.prototype; /** * @return {HTMLDomElement} */ echartsProto.getDom = function () { return this._dom; }; /** * @return {module:zrender~ZRender} */ echartsProto.getZr = function () { return this._zr; }; /** * @param {Object} option * @param {boolean} notMerge * @param {boolean} [notRefreshImmediately=false] */ echartsProto.setOption = function (option, notMerge, notRefreshImmediately) { // PENDING option = zrUtil.clone(option, true); each(optionPreprocessorFuncs, function (preProcess) { preProcess(option); }); var ecModel = this._model; if (!ecModel || notMerge) { ecModel = new GlobalModel(option, null, this._theme); this._model = ecModel; } else { ecModel.restoreData(); ecModel.mergeOption(option); } prepareView.call(this, 'component', ecModel); prepareView.call(this, 'chart', ecModel); updateMethods.update.call(this); !notRefreshImmediately && this._zr.refreshImmediately(); }; /** * @DEPRECATED */ echartsProto.setTheme = function () { console.log('ECharts#setTheme() is DEPRECATED in ECharts 3.0'); }; /** * @return {module:echarts/model/Global} */ echartsProto.getModel = function () { return this._model; }; /** * @return {number} */ echartsProto.getWidth = function () { return this._zr.getWidth(); }; /** * @return {number} */ echartsProto.getHeight = function () { return this._zr.getHeight(); }; var updateMethods = { /** * @param {Object} payload * @private */ update: function (payload) { // console.time && console.time('update'); var ecModel = this._model; // update before setOption if (!ecModel) { return; } ecModel.restoreData(); // 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. processData.call(this, ecModel); stackSeriesData.call(this, ecModel); this._coordinateSystem.update(ecModel, this._api); doLayout.call(this, ecModel, payload); doVisualCoding.call(this, ecModel, payload); doRender.call(this, ecModel, payload); // Set background var backgroundColor = ecModel.get('backgroundColor'); // In IE8 if (!env.canvasSupported) { var colorArr = colorTool.parse(backgroundColor); backgroundColor = colorTool.stringify(colorArr, 'rgb'); if (colorArr[3] === 0) { backgroundColor = 'transparent'; } } if (env.node) { this._zr.configLayer(0, { clearColor: backgroundColor }); } else { backgroundColor && (this._dom.style.backgroundColor = backgroundColor); } // console.time && console.timeEnd('update'); }, // PENDING /** * @param {Object} payload * @private */ updateView: function (payload) { var ecModel = this._model; // update before setOption if (!ecModel) { return; } doLayout.call(this, ecModel, payload); doVisualCoding.call(this, ecModel, payload); invokeUpdateMethod.call(this, 'updateView', ecModel, payload); }, /** * @param {Object} payload * @private */ updateVisual: function (payload) { var ecModel = this._model; // update before setOption if (!ecModel) { return; } doVisualCoding.call(this, ecModel, payload); invokeUpdateMethod.call(this, 'updateVisual', ecModel, payload); }, /** * @param {Object} payload * @private */ updateLayout: function (payload) { var ecModel = this._model; // update before setOption if (!ecModel) { return; } doLayout.call(this, ecModel, payload); invokeUpdateMethod.call(this, 'updateLayout', ecModel, payload); }, /** * @param {Object} payload * @private */ highlight: function (payload) { toggleHighlight.call(this, 'highlight', payload); }, /** * @param {Object} payload * @private */ downplay: function (payload) { toggleHighlight.call(this, 'downplay', payload); } }; /** * @param {Object} payload * @private */ function toggleHighlight(method, payload) { var ecModel = this._model; // dispatchAction before setOption if (!ecModel) { return; } ecModel.eachComponent( {mainType: 'series', query: payload}, function (seriesModel, index, payloadInfo) { var chartView = this._chartsMap[seriesModel.id]; if (chartView) { chartView[method]( seriesModel, ecModel, this._api, payloadInfo ); } }, this ); } /** * Resize the chart */ echartsProto.resize = function () { this._zr.resize(); updateMethods.update.call(this); // Resize loading effect this._loadingFX && this._loadingFX.resize(); }; var defaultLoadingEffect = require('./loading/default'); /** * Show loading effect * @param {string} [name='default'] * @param {Object} [cfg] */ echartsProto.showLoading = function (name, cfg) { if (zrUtil.isObject(name)) { cfg = name; name = 'default'; } var el = defaultLoadingEffect(this._api, cfg); var zr = this._zr; this._loadingFX = el; zr.painter.clear(); zr.add(el); }; /** * Hide loading effect */ echartsProto.hideLoading = function () { this._zr.remove(this._loadingFX); this._loadingFX = null; }; /** * @param {Object} eventObj * @return {Object} */ echartsProto.makeActionFromEvent = function (eventObj) { var payload = zrUtil.extend({}, eventObj); payload.type = eventActionMap[eventObj.type]; return payload; }; /** * @pubilc * @param {Object} payload * @param {string} [payload.type] Action type * @param {boolean} [silent=false] Whether trigger event. * @param {number} [payload.from] From uid */ echartsProto.dispatchAction = function (payload, silent) { var actionWrap = actions[payload.type]; if (actionWrap) { var actionInfo = actionWrap.actionInfo; var updateMethod = actionInfo.update || 'update'; actionWrap.action(payload, this._model); updateMethod !== 'none' && updateMethods[updateMethod].call(this, payload); if (!silent) { // Emit event outside // Convert type to eventType var eventObj = zrUtil.extend({}, payload); eventObj.type = actionInfo.event || eventObj.type; this._messageCenter.trigger(eventObj.type, eventObj); } } }; /** * @param {string} methodName * @private */ function invokeUpdateMethod(methodName, ecModel, payload) { var api = this._api; // Update all components each(this._componentsList, function (component) { var componentModel = component.__model; component[methodName](componentModel, ecModel, api, payload); updateZ(componentModel, component); }, this); // Upate all charts ecModel.eachSeries(function (seriesModel, idx) { var chart = this._chartsMap[seriesModel.id]; chart[methodName](seriesModel, ecModel, api, payload); updateZ(seriesModel, chart); }, this); } /** * Prepare view instances of charts and components * @param {module:echarts/model/Global} ecModel * @private */ function prepareView(type, ecModel) { var isComponent = type === 'component'; var viewList = isComponent ? this._componentsList : this._chartsList; var viewMap = isComponent ? this._componentsMap : this._chartsMap; var zr = this._zr; for (var i = 0; i < viewList.length; i++) { viewList[i].__keepAlive = false; } ecModel[isComponent ? 'eachComponent' : 'eachSeries'](function (componentType, model) { if (isComponent) { if (componentType === 'series') { return; } } else { model = componentType; } var view = viewMap[model.id]; if (!view) { var classType = ComponentModel.parseClassType(model.type); var Clazz = isComponent ? ComponentView.getClass(classType.main, classType.sub) : ChartView.getClass(classType.sub); if (Clazz) { view = new Clazz(); view.init(ecModel, this._api); viewMap[model.id] = view; viewList.push(view); zr.add(view.group); } else { // Error return; } } view.__keepAlive = true; view.__id = model.id; view.__model = model; }, this); for (var i = 0; i < viewList.length;) { var view = viewList[i]; if (!view.__keepAlive) { zr.remove(view.group); view.dispose(this._api); viewList.splice(i, 1); delete viewMap[view.__id]; } else { i++; } } } /** * Processor data in each series * * @param {module:echarts/model/Global} ecModel * @private */ function processData(ecModel) { each(PROCESSOR_STAGES, function (stage) { each(dataProcessorFuncs[stage] || [], function (process) { process(ecModel); }); }); } /** * @private */ function stackSeriesData(ecModel) { var stackedDataMap = {}; ecModel.eachSeries(function (series) { var stack = series.get('stack'); var data = series.getData(); if (stack && data.type === 'list') { var previousStack = stackedDataMap[stack]; if (previousStack) { data.stackedOn = previousStack; } stackedDataMap[stack] = data; } }); } /** * Layout before each chart render there series, after visual coding and data processing * * @param {module:echarts/model/Global} ecModel * @private */ function doLayout(ecModel, payload) { var api = this._api; each(layoutFuncs, function (layout) { layout(ecModel, api, payload); }); } /** * Code visual infomation from data after data processing * * @param {module:echarts/model/Global} ecModel * @private */ function doVisualCoding(ecModel, payload) { each(VISUAL_CODING_STAGES, function (stage) { each(visualCodingFuncs[stage] || [], function (visualCoding) { visualCoding(ecModel, payload); }); }); } /** * Render each chart and component * @private */ function doRender(ecModel, payload) { var api = this._api; // Render all components each(this._componentsList, function (component) { var componentModel = component.__model; component.render(componentModel, ecModel, api, payload); updateZ(componentModel, component); }, this); each(this._chartsList, function (chart) { chart.__keepAlive = false; }, this); // Render all charts ecModel.eachSeries(function (seriesModel, idx) { var chart = this._chartsMap[seriesModel.id]; chart.__keepAlive = true; chart.render(seriesModel, ecModel, api, payload); updateZ(seriesModel, chart); }, this); // Remove groups of unrendered charts each(this._chartsList, function (chart) { if (!chart.__keepAlive) { chart.remove(ecModel, api); } }, this); } var MOUSE_EVENT_NAMES = [ 'click', 'dblclick', 'mouseover', 'mouseout', 'globalout' ]; /** * @private */ echartsProto._initEvents = function () { var zr = this._zr; each(MOUSE_EVENT_NAMES, function (eveName) { zr.on(eveName, function (e) { var ecModel = this.getModel(); var el = e.target; if (el && el.dataIndex != null) { var hostModel = el.hostModel || ecModel.getSeriesByIndex(el.seriesIndex); var params = hostModel && hostModel.getDataParams(el.dataIndex) || {}; params.event = e; params.type = eveName; this.trigger(eveName, params); } }, this); }, this); zrUtil.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; }; /** * Dispose instance */ echartsProto.dispose = function () { this._disposed = true; each(this._components, function (component) { component.dispose(); }); each(this._charts, function (chart) { chart.dispose(); }); this._zr.dispose(); instances[this.id] = null; }; zrUtil.mixin(ECharts, Eventful); /** * @param {module:echarts/model/Series|module:echarts/model/Component} model * @param {module:echarts/view/Component|module:echarts/view/Chart} view * @return {string} */ function updateZ(model, view) { var z = model.get('z'); var zlevel = model.get('zlevel'); // Set z and zlevel view.group.traverse(function (el) { z != null && (el.z = z); zlevel != null && (el.zlevel = zlevel); }); } /** * @type {Array.} * @inner */ var actions = []; /** * Map eventType to actionType * @type {Object} */ var eventActionMap = {}; /** * @type {Array.} * @inner */ var layoutFuncs = []; /** * Data processor functions of each stage * @type {Array.>} * @inner */ var dataProcessorFuncs = {}; /** * @type {Array.} * @inner */ var optionPreprocessorFuncs = []; /** * Visual coding functions of each stage * @type {Array.>} * @inner */ var visualCodingFuncs = {}; var instances = {}; var connectedGroups = {}; var idBase = new Date() - 0; var groupIdBase = new Date() - 0; var DOM_ATTRIBUTE_KEY = '_echarts_instance_'; /** * @alias module:echarts */ var echarts = { /** * @type {number} */ version: '3.0.0', dependencies: { zrender: '3.0.0' } }; /** * @param {HTMLDomElement} dom * @param {Object} [theme] * @param {Object} opts */ echarts.init = function (dom, theme, opts) { // Check version if ((zrender.version.replace('.', '') - 0) < (echarts.dependencies.zrender.replace('.', '') - 0)) { console.error( 'ZRender ' + zrender.version + ' is too old for ECharts ' + echarts.version + '. Current version need ZRender ' + echarts.dependencies.zrender + '+' ); } var chart = new ECharts(dom, theme, opts); chart.id = idBase++; instances[chart.id] = chart; dom.setAttribute && dom.setAttribute(DOM_ATTRIBUTE_KEY, chart.id); // Connecting zrUtil.each(eventActionMap, function (actionType, eventType) { // FIXME chart._messageCenter.on(eventType, function (event) { if (connectedGroups[chart.group]) { chart.__connectedActionDispatching = true; for (var id in instances) { var action = chart.makeActionFromEvent(event); var otherChart = instances[id]; if (otherChart !== chart && otherChart.group === chart.group) { if (!otherChart.__connectedActionDispatching) { otherChart.dispatchAction(action); } } } chart.__connectedActionDispatching = false; } }); }); return chart; }; /** * @return {string|Array.} groupId */ echarts.connect = function (groupId) { // Is array of charts if (zrUtil.isArray(groupId)) { var charts = groupId; groupId = null; // If any chart has group zrUtil.each(charts, function (chart) { if (chart.group != null) { groupId = chart.group; } }); groupId = groupId || groupIdBase++; zrUtil.each(charts, function (chart) { chart.group = groupId; }); } connectedGroups[groupId] = true; return groupId; }; /** * @return {string} groupId */ echarts.disConnect = function (groupId) { connectedGroups[groupId] = false; }; /** * Dispose a chart instance * @param {module:echarts~ECharts|HTMLDomElement|string} chart */ echarts.dispose = function (chart) { if (zrUtil.isDom(chart)) { chart = echarts.getInstanceByDom(chart); } else if (typeof chart === 'string') { chart = instances[chart]; } if ((chart instanceof ECharts) && !chart.isDisposed()) { chart.dispose(); } }; /** * @param {HTMLDomElement} dom * @return {echarts~ECharts} */ echarts.getInstanceByDom = function (dom) { var key = dom.getAttribute(DOM_ATTRIBUTE_KEY); return instances[key]; }; /** * @param {string} key * @return {echarts~ECharts} */ echarts.getInstanceById = function (key) { return instances[key]; }; /** * Register option preprocessor * @param {Function} preprocessorFunc */ echarts.registerPreprocessor = function (preprocessorFunc) { optionPreprocessorFuncs.push(preprocessorFunc); }; /** * @param {string} stage * @param {Function} processorFunc */ echarts.registerProcessor = function (stage, processorFunc) { if (zrUtil.indexOf(PROCESSOR_STAGES, stage) < 0) { throw new Error('stage should be one of ' + PROCESSOR_STAGES); } var funcs = dataProcessorFuncs[stage] || (dataProcessorFuncs[stage] = []); funcs.push(processorFunc); }; /** * 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 */ echarts.registerAction = function (actionInfo, eventName, action) { if (typeof eventName === 'function') { action = eventName; eventName = ''; } var actionType = zrUtil.isObject(actionInfo) ? actionInfo.type : ([actionInfo, actionInfo = { event: eventName }][0]); actionInfo.event = actionInfo.event || actionType; eventName = actionInfo.event; if (!actions[actionType]) { actions[actionType] = {action: action, actionInfo: actionInfo}; } eventActionMap[eventName] = actionType; }; /** * @param {string} type * @param {*} CoordinateSystem */ echarts.registerCoordinateSystem = function (type, CoordinateSystem) { CoordinateSystemManager.register(type, CoordinateSystem); }; /** * @param {*} layout */ echarts.registerLayout = function (layout) { // PENDING All functions ? if (zrUtil.indexOf(layoutFuncs, layout) < 0) { layoutFuncs.push(layout); } }; /** * @param {string} stage * @param {Function} visualCodingFunc */ echarts.registerVisualCoding = function (stage, visualCodingFunc) { if (zrUtil.indexOf(VISUAL_CODING_STAGES, stage) < 0) { throw new Error('stage should be one of ' + VISUAL_CODING_STAGES); } var funcs = visualCodingFuncs[stage] || (visualCodingFuncs[stage] = []); funcs.push(visualCodingFunc); }; /** * @param {Object} opts */ echarts.extendChartView = function (opts) { return ChartView.extend(opts); }; /** * @param {Object} opts */ echarts.extendComponentModel = function (opts) { return ComponentModel.extend(opts); }; /** * @param {Object} opts */ echarts.extendSeriesModel = function (opts) { return SeriesModel.extend(opts); }; /** * @param {Object} opts */ echarts.extendComponentView = function (opts) { return ComponentView.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); * }); */ echarts.setCanvasCreator = function (creator) { zrUtil.createCanvas = creator; }; echarts.registerVisualCoding('echarts', zrUtil.curry( require('./visual/seriesColor'), '', 'itemStyle' )); echarts.registerPreprocessor(require('./preprocessor/backwardCompat')); // Default action echarts.registerAction({ type: 'highlight', event: 'highlight', update: 'highlight' }, zrUtil.noop); echarts.registerAction({ type: 'downplay', event: 'downplay', update: 'downplay' }, zrUtil.noop); // -------- // Exports // -------- echarts.graphic = require('echarts/util/graphic'); echarts.number = require('echarts/util/number'); echarts.format = require('echarts/util/format'); echarts.util = {}; each([ 'map', 'each', 'filter', 'indexOf', 'inherits', 'reduce', 'filter', 'bind', 'curry', 'isArray', 'isString', 'isObject', 'isFunction', 'extend' ], function (name) { echarts.util[name] = zrUtil[name]; } ); return echarts; });