/** * @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 Scale = require('./scale/Scale'); 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~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' }); /** * @type {Object} * @private */ this._theme = zrUtil.clone(theme); /** * @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); // 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); } this._prepareView('component', ecModel); this._prepareView('chart', ecModel); this._update(); !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(); }; /** * @param {Object} payload * @private */ echartsProto._update = function (payload) { console.time && console.time('update'); var ecModel = this._model; 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. this._processData(ecModel); this._stackSeriesData(ecModel); this._coordinateSystem.update(ecModel, this._api); this._doLayout(ecModel, payload); this._doVisualCoding(ecModel, payload); this._doRender(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'; } } backgroundColor && (this._dom.style.backgroundColor = backgroundColor); console.time && console.timeEnd('update'); }; // PENDING /** * @param {Object} payload * @private */ echartsProto._updateView = function (payload) { var ecModel = this._model; this._doLayout(ecModel, payload); this._doVisualCoding(ecModel, payload); this._invokeUpdateMethod('updateView', ecModel, payload); }; /** * @param {Object} payload * @private */ echartsProto._updateVisual = function (payload) { var ecModel = this._model; this._doVisualCoding(ecModel, payload); this._invokeUpdateMethod('updateVisual', ecModel, payload); }; /** * @param {Object} payload * @private */ echartsProto._updateLayout = function (payload) { var ecModel = this._model; this._doLayout(ecModel, payload); this._invokeUpdateMethod('updateLayout', ecModel, payload); }; /** * @param {Object} payload * @private */ echartsProto._highlight = function (payload) { this._toggleHighlight('highlight', payload); }; /** * @param {Object} payload * @private */ echartsProto._downplay = function (payload) { this._toggleHighlight('downplay', payload); }; /** * @param {Object} payload * @private */ echartsProto._toggleHighlight = function (method, payload) { var ecModel = this._model; var seriesModel; if (payload.seriesIndex) { seriesModel = ecModel.getSeriesByIndex(payload.seriesIndex, true); } else if (payload.seriesName) { seriesModel = ecModel.getSeriesByName(payload.seriesName, true); } if (seriesModel) { var chartView = this._chartsMap[seriesModel.getId()]; if (chartView) { chartView[method](seriesModel, ecModel, this._api, payload); } } }; /** * Resize the chart */ echartsProto.resize = function () { this._zr.resize(); this._update(); }; /** * @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.dispatch = 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' && this['_' + updateMethod](payload); if (!silent) { // Emit event outside // Convert type to eventType var eventObj = zrUtil.extend({}, payload); eventObj.type = actionInfo.event || eventObj.type; this.trigger(eventObj.type, eventObj); } } }; /** * @param {string} methodName * @private */ echartsProto._invokeUpdateMethod = function (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.getId()]; chart[methodName](seriesModel, ecModel, api, payload); updateZ(seriesModel, chart); }, this); }; /** * Prepare view instances of charts and components * @param {module:echarts/model/Global} ecModel * @private */ echartsProto._prepareView = function (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 id = model.getId(); var view = viewMap[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[id] = view; viewList.push(view); zr.add(view.group); } else { // Error } } view.__keepAlive = true; view.__id = 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 */ echartsProto._processData = function (ecModel) { each(PROCESSOR_STAGES, function (stage) { each(dataProcessorFuncs[stage] || [], function (process) { process(ecModel); }); }); }; /** * @private */ echartsProto._stackSeriesData = function (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 */ echartsProto._doLayout = function (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 */ echartsProto._doVisualCoding = function (ecModel, payload) { each(VISUAL_CODING_STAGES, function (stage) { each(visualCodingFuncs[stage] || [], function (visualCoding) { visualCoding(ecModel, payload); }); }); }; /** * Render each chart and component * @private */ echartsProto._doRender = function (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.getId()]; 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, true ); var params = hostModel && hostModel.getDataParams(el.dataIndex) || {}; params.event = e; params.type = eveName; this.trigger(eveName, params); } }, 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; // Connecting zrUtil.each(eventActionMap, function (actionType, eventType) { chart.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.dispatch(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 {echarts/scale/*} scale */ // echarts.extendScale = function (opts) { // return Scale.extend(opts); // }; /** * @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); }; echarts.registerVisualCoding('echarts', require('./visual/seriesColor')); 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); return echarts; });