| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import * as echarts from '../../core/echarts'; |
| import { createHashMap, each, HashMap, hasOwn, keys, map } from 'zrender/src/core/util'; |
| import SeriesModel from '../../model/Series'; |
| import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; |
| import { getDataDimensionsOnAxis, unionAxisExtentFromData } from '../axisHelper'; |
| import { AxisBaseModel } from '../AxisBaseModel'; |
| import Axis from '../Axis'; |
| import GlobalModel from '../../model/Global'; |
| import { Dictionary } from '../../util/types'; |
| import { ScaleRawExtentInfo, ScaleRawExtentResult, ensureScaleRawExtentInfo } from '../scaleRawExtentInfo'; |
| |
| |
| type AxisRecord = { |
| condExtent: number[]; |
| rawExtentInfo?: ScaleRawExtentInfo; |
| rawExtentResult?: ScaleRawExtentResult |
| tarExtent?: number[]; |
| }; |
| |
| type SeriesRecord = { |
| seriesModel: SeriesModel; |
| xAxisModel: AxisBaseModel; |
| yAxisModel: AxisBaseModel; |
| }; |
| |
| // A tricky: the priority is just after dataZoom processor. |
| // If dataZoom has fixed the min/max, this processor do not need to work. |
| // TODO: SELF REGISTERED. |
| echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER + 10, { |
| |
| getTargetSeries: function (ecModel) { |
| const seriesModelMap = createHashMap<SeriesModel>(); |
| ecModel.eachSeries(function (seriesModel: SeriesModel) { |
| isCartesian2DSeries(seriesModel) && seriesModelMap.set(seriesModel.uid, seriesModel); |
| }); |
| return seriesModelMap; |
| }, |
| |
| overallReset: function (ecModel, api) { |
| const seriesRecords = [] as SeriesRecord[]; |
| const axisRecordMap = createHashMap<AxisRecord>(); |
| |
| prepareDataExtentOnAxis(ecModel, axisRecordMap, seriesRecords); |
| calculateFilteredExtent(axisRecordMap, seriesRecords); |
| shrinkAxisExtent(axisRecordMap); |
| } |
| }); |
| |
| function prepareDataExtentOnAxis( |
| ecModel: GlobalModel, |
| axisRecordMap: HashMap<AxisRecord>, |
| seriesRecords: SeriesRecord[] |
| ): void { |
| ecModel.eachSeries(function (seriesModel: SeriesModel) { |
| if (!isCartesian2DSeries(seriesModel)) { |
| return; |
| } |
| |
| const axesModelMap = findAxisModels(seriesModel); |
| const xAxisModel = axesModelMap.xAxisModel; |
| const yAxisModel = axesModelMap.yAxisModel; |
| const xAxis = xAxisModel.axis; |
| const yAxis = yAxisModel.axis; |
| const xRawExtentInfo = xAxis.scale.rawExtentInfo; |
| const yRawExtentInfo = yAxis.scale.rawExtentInfo; |
| const data = seriesModel.getData(); |
| |
| // If either axis controlled by other filter like "dataZoom", |
| // use the rule of dataZoom rather than adopting the rules here. |
| if ( |
| (xRawExtentInfo && xRawExtentInfo.frozen) |
| || (yRawExtentInfo && yRawExtentInfo.frozen) |
| ) { |
| return; |
| } |
| |
| seriesRecords.push({ |
| seriesModel: seriesModel, |
| xAxisModel: xAxisModel, |
| yAxisModel: yAxisModel |
| }); |
| |
| // FIXME: this logic needs to be consistent with |
| // `coord/cartesian/Grid.ts#_updateScale`. |
| // It's not good to implement one logic in multiple places. |
| unionAxisExtentFromData(prepareAxisRecord(axisRecordMap, xAxisModel).condExtent, data, xAxis.dim); |
| unionAxisExtentFromData(prepareAxisRecord(axisRecordMap, yAxisModel).condExtent, data, yAxis.dim); |
| }); |
| } |
| |
| function calculateFilteredExtent( |
| axisRecordMap: HashMap<AxisRecord>, |
| seriesRecords: SeriesRecord[] |
| ) { |
| each(seriesRecords, function (seriesRecord) { |
| const xAxisModel = seriesRecord.xAxisModel; |
| const yAxisModel = seriesRecord.yAxisModel; |
| const xAxis = xAxisModel.axis; |
| const yAxis = yAxisModel.axis; |
| const xAxisRecord = prepareAxisRecord(axisRecordMap, xAxisModel); |
| const yAxisRecord = prepareAxisRecord(axisRecordMap, yAxisModel); |
| xAxisRecord.rawExtentInfo = ensureScaleRawExtentInfo( |
| xAxis.scale, xAxisModel, xAxisRecord.condExtent |
| ); |
| yAxisRecord.rawExtentInfo = ensureScaleRawExtentInfo( |
| yAxis.scale, yAxisModel, yAxisRecord.condExtent |
| ); |
| xAxisRecord.rawExtentResult = xAxisRecord.rawExtentInfo.calculate(); |
| yAxisRecord.rawExtentResult = yAxisRecord.rawExtentInfo.calculate(); |
| |
| // If the "xAxis" is set `min`/`max`, some data items might be out of the cartesian. |
| // then the "yAxis" may needs to calculate extent only based on the data items inside |
| // the cartesian (similar to what "dataZoom" did). |
| // A typical case is bar-racing, where bars ara sort dynamically and may only need to |
| // displayed part of the whole bars. |
| |
| const data = seriesRecord.seriesModel.getData(); |
| // For duplication removal. |
| const condDimMap: Dictionary<boolean> = {}; |
| const tarDimMap: Dictionary<boolean> = {}; |
| let condAxis: Axis; |
| let tarAxisRecord: AxisRecord; |
| |
| function addCondition(axis: Axis, axisRecord: AxisRecord) { |
| // But for simplicity and safty and performance, we only adopt this |
| // feature on category axis at present. |
| const condExtent = axisRecord.condExtent; |
| const rawExtentResult = axisRecord.rawExtentResult; |
| if (axis.type === 'category' |
| && (condExtent[0] < rawExtentResult.min || rawExtentResult.max < condExtent[1]) |
| ) { |
| each(getDataDimensionsOnAxis(data, axis.dim), function (dataDim) { |
| if (!hasOwn(condDimMap, dataDim)) { |
| condDimMap[dataDim] = true; |
| condAxis = axis; |
| } |
| }); |
| } |
| } |
| function addTarget(axis: Axis, axisRecord: AxisRecord) { |
| const rawExtentResult = axisRecord.rawExtentResult; |
| if (axis.type !== 'category' |
| && (!rawExtentResult.minFixed || !rawExtentResult.maxFixed) |
| ) { |
| each(getDataDimensionsOnAxis(data, axis.dim), function (dataDim) { |
| if (!hasOwn(condDimMap, dataDim) && !hasOwn(tarDimMap, dataDim)) { |
| tarDimMap[dataDim] = true; |
| tarAxisRecord = axisRecord; |
| } |
| }); |
| } |
| } |
| |
| addCondition(xAxis, xAxisRecord); |
| addCondition(yAxis, yAxisRecord); |
| addTarget(xAxis, xAxisRecord); |
| addTarget(yAxis, yAxisRecord); |
| |
| const condDims = keys(condDimMap); |
| const tarDims = keys(tarDimMap); |
| const tarDimExtents = map(tarDims, function () { |
| return initExtent(); |
| }); |
| |
| const condDimsLen = condDims.length; |
| const tarDimsLen = tarDims.length; |
| |
| if (!condDimsLen || !tarDimsLen) { |
| return; |
| } |
| |
| const singleCondDim = condDimsLen === 1 ? condDims[0] : null; |
| const singleTarDim = tarDimsLen === 1 ? tarDims[0] : null; |
| const dataLen = data.count(); |
| |
| // Time consuming, because this is a "block task". |
| // Simple optimization for the vast majority of cases. |
| if (singleCondDim && singleTarDim) { |
| for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) { |
| const condVal = data.get(singleCondDim, dataIdx) as number; |
| if (condAxis.scale.isInExtentRange(condVal)) { |
| unionExtent(tarDimExtents[0], data.get(singleTarDim, dataIdx) as number); |
| } |
| } |
| } |
| else { |
| for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) { |
| for (let j = 0; j < condDimsLen; j++) { |
| const condVal = data.get(condDims[j], dataIdx) as number; |
| if (condAxis.scale.isInExtentRange(condVal)) { |
| for (let k = 0; k < tarDimsLen; k++) { |
| unionExtent(tarDimExtents[k], data.get(tarDims[k], dataIdx) as number); |
| } |
| // Any one dim is in range means satisfied. |
| break; |
| } |
| } |
| } |
| } |
| |
| each(tarDimExtents, function (tarDimExtent, i) { |
| const dim = tarDims[i]; |
| // FIXME: if there has been approximateExtent set? |
| data.setApproximateExtent(tarDimExtent as [number, number], dim); |
| const tarAxisExtent = tarAxisRecord.tarExtent = tarAxisRecord.tarExtent || initExtent(); |
| unionExtent(tarAxisExtent, tarDimExtent[0]); |
| unionExtent(tarAxisExtent, tarDimExtent[1]); |
| }); |
| }); |
| } |
| |
| function shrinkAxisExtent(axisRecordMap: HashMap<AxisRecord>) { |
| axisRecordMap.each(function (axisRecord) { |
| const tarAxisExtent = axisRecord.tarExtent; |
| if (tarAxisExtent) { |
| const rawExtentResult = axisRecord.rawExtentResult; |
| const rawExtentInfo = axisRecord.rawExtentInfo; |
| // Shink the original extent. |
| if (!rawExtentResult.minFixed && tarAxisExtent[0] > rawExtentResult.min) { |
| rawExtentInfo.modifyDataMinMax('min', tarAxisExtent[0]); |
| } |
| if (!rawExtentResult.maxFixed && tarAxisExtent[1] < rawExtentResult.max) { |
| rawExtentInfo.modifyDataMinMax('max', tarAxisExtent[1]); |
| } |
| } |
| }); |
| } |
| |
| function prepareAxisRecord( |
| axisRecordMap: HashMap<AxisRecord>, |
| axisModel: AxisBaseModel |
| ): AxisRecord { |
| return axisRecordMap.get(axisModel.uid) |
| || axisRecordMap.set(axisModel.uid, { condExtent: initExtent() }); |
| } |
| |
| function initExtent() { |
| return [Infinity, -Infinity]; |
| } |
| |
| function unionExtent(extent: number[], val: number) { |
| val < extent[0] && (extent[0] = val); |
| val > extent[1] && (extent[1] = val); |
| } |