| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT 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 |
| */ |
| import { each, map, isFunction, createHashMap, noop } from 'zrender/src/core/util'; |
| import { createTask } from './task'; |
| import { getUID } from '../util/component'; |
| import GlobalModel from '../model/Global'; |
| import ExtensionAPI from '../ExtensionAPI'; |
| import { normalizeToArray } from '../util/model'; |
| /** |
| * @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(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(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(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(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 */ |
| |
| } |
| |
| export default Scheduler; |