Spaces:
Running
Running
| /* | |
| * 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 SeriesData from '../../data/SeriesData'; | |
| import * as numberUtil from '../../util/number'; | |
| import * as markerHelper from './markerHelper'; | |
| import LineDraw from '../../chart/helper/LineDraw'; | |
| import MarkerView from './MarkerView'; | |
| import {getStackedDimension} from '../../data/helper/dataStackHelper'; | |
| import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; | |
| import MarkLineModel, { MarkLine2DDataItemOption, MarkLineOption } from './MarkLineModel'; | |
| import { ScaleDataValue, ColorString } from '../../util/types'; | |
| import SeriesModel from '../../model/Series'; | |
| import { getECData } from '../../util/innerStore'; | |
| import ExtensionAPI from '../../core/ExtensionAPI'; | |
| import Cartesian2D from '../../coord/cartesian/Cartesian2D'; | |
| import GlobalModel from '../../model/Global'; | |
| import MarkerModel from './MarkerModel'; | |
| import { | |
| isArray, | |
| retrieve, | |
| retrieve2, | |
| clone, | |
| extend, | |
| logError, | |
| merge, | |
| map, | |
| curry, | |
| filter, | |
| HashMap, | |
| isNumber | |
| } from 'zrender/src/core/util'; | |
| import { makeInner } from '../../util/model'; | |
| import { LineDataVisual } from '../../visual/commonVisualTypes'; | |
| import { getVisualFromData } from '../../visual/helper'; | |
| import Axis2D from '../../coord/cartesian/Axis2D'; | |
| import SeriesDimensionDefine from '../../data/SeriesDimensionDefine'; | |
| // Item option for configuring line and each end of symbol. | |
| // Line option. be merged from configuration of two ends. | |
| type MarkLineMergedItemOption = MarkLine2DDataItemOption[number]; | |
| const inner = makeInner<{ | |
| // from data | |
| from: SeriesData<MarkLineModel> | |
| // to data | |
| to: SeriesData<MarkLineModel> | |
| }, MarkLineModel>(); | |
| const markLineTransform = function ( | |
| seriesModel: SeriesModel, | |
| coordSys: CoordinateSystem, | |
| mlModel: MarkLineModel, | |
| item: MarkLineOption['data'][number] | |
| ) { | |
| const data = seriesModel.getData(); | |
| let itemArray: MarkLineMergedItemOption[]; | |
| if (!isArray(item)) { | |
| // Special type markLine like 'min', 'max', 'average', 'median' | |
| const mlType = item.type; | |
| if ( | |
| mlType === 'min' || mlType === 'max' || mlType === 'average' || mlType === 'median' | |
| // In case | |
| // data: [{ | |
| // yAxis: 10 | |
| // }] | |
| || (item.xAxis != null || item.yAxis != null) | |
| ) { | |
| let valueAxis; | |
| let value; | |
| if (item.yAxis != null || item.xAxis != null) { | |
| valueAxis = coordSys.getAxis(item.yAxis != null ? 'y' : 'x'); | |
| value = retrieve(item.yAxis, item.xAxis); | |
| } | |
| else { | |
| const axisInfo = markerHelper.getAxisInfo(item, data, coordSys, seriesModel); | |
| valueAxis = axisInfo.valueAxis; | |
| const valueDataDim = getStackedDimension(data, axisInfo.valueDataDim); | |
| value = markerHelper.numCalculate(data, valueDataDim, mlType); | |
| } | |
| const valueIndex = valueAxis.dim === 'x' ? 0 : 1; | |
| const baseIndex = 1 - valueIndex; | |
| // Normized to 2d data with start and end point | |
| const mlFrom = clone(item) as MarkLine2DDataItemOption[number]; | |
| const mlTo = { | |
| coord: [] | |
| } as MarkLine2DDataItemOption[number]; | |
| mlFrom.type = null; | |
| mlFrom.coord = []; | |
| mlFrom.coord[baseIndex] = -Infinity; | |
| mlTo.coord[baseIndex] = Infinity; | |
| const precision = mlModel.get('precision'); | |
| if (precision >= 0 && isNumber(value)) { | |
| value = +value.toFixed(Math.min(precision, 20)); | |
| } | |
| mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value; | |
| itemArray = [mlFrom, mlTo, { // Extra option for tooltip and label | |
| type: mlType, | |
| valueIndex: item.valueIndex, | |
| // Force to use the value of calculated value. | |
| value: value | |
| }]; | |
| } | |
| else { | |
| // Invalid data | |
| if (__DEV__) { | |
| logError('Invalid markLine data.'); | |
| } | |
| itemArray = []; | |
| } | |
| } | |
| else { | |
| itemArray = item; | |
| } | |
| const normalizedItem = [ | |
| markerHelper.dataTransform(seriesModel, itemArray[0]), | |
| markerHelper.dataTransform(seriesModel, itemArray[1]), | |
| extend({}, itemArray[2]) | |
| ]; | |
| // Avoid line data type is extended by from(to) data type | |
| normalizedItem[2].type = normalizedItem[2].type || null; | |
| // Merge from option and to option into line option | |
| merge(normalizedItem[2], normalizedItem[0]); | |
| merge(normalizedItem[2], normalizedItem[1]); | |
| return normalizedItem; | |
| }; | |
| function isInfinity(val: ScaleDataValue) { | |
| return !isNaN(val as number) && !isFinite(val as number); | |
| } | |
| // If a markLine has one dim | |
| function ifMarkLineHasOnlyDim( | |
| dimIndex: number, | |
| fromCoord: ScaleDataValue[], | |
| toCoord: ScaleDataValue[], | |
| coordSys: CoordinateSystem | |
| ) { | |
| const otherDimIndex = 1 - dimIndex; | |
| const dimName = coordSys.dimensions[dimIndex]; | |
| return isInfinity(fromCoord[otherDimIndex]) && isInfinity(toCoord[otherDimIndex]) | |
| && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]); | |
| } | |
| function markLineFilter( | |
| coordSys: CoordinateSystem, | |
| item: MarkLine2DDataItemOption | |
| ) { | |
| if (coordSys.type === 'cartesian2d') { | |
| const fromCoord = item[0].coord; | |
| const toCoord = item[1].coord; | |
| // In case | |
| // { | |
| // markLine: { | |
| // data: [{ yAxis: 2 }] | |
| // } | |
| // } | |
| if ( | |
| fromCoord && toCoord | |
| && (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys) | |
| || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys)) | |
| ) { | |
| return true; | |
| } | |
| } | |
| return markerHelper.dataFilter(coordSys, item[0]) | |
| && markerHelper.dataFilter(coordSys, item[1]); | |
| } | |
| function updateSingleMarkerEndLayout( | |
| data: SeriesData<MarkLineModel>, | |
| idx: number, | |
| isFrom: boolean, | |
| seriesModel: SeriesModel, | |
| api: ExtensionAPI | |
| ) { | |
| const coordSys = seriesModel.coordinateSystem; | |
| const itemModel = data.getItemModel<MarkLine2DDataItemOption[number]>(idx); | |
| let point; | |
| const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth()); | |
| const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight()); | |
| if (!isNaN(xPx) && !isNaN(yPx)) { | |
| point = [xPx, yPx]; | |
| } | |
| else { | |
| // Chart like bar may have there own marker positioning logic | |
| if (seriesModel.getMarkerPosition) { | |
| // Use the getMarkerPosition | |
| point = seriesModel.getMarkerPosition( | |
| data.getValues(data.dimensions, idx) | |
| ); | |
| } | |
| else { | |
| const dims = coordSys.dimensions; | |
| const x = data.get(dims[0], idx); | |
| const y = data.get(dims[1], idx); | |
| point = coordSys.dataToPoint([x, y]); | |
| } | |
| // Expand line to the edge of grid if value on one axis is Inifnity | |
| // In case | |
| // markLine: { | |
| // data: [{ | |
| // yAxis: 2 | |
| // // or | |
| // type: 'average' | |
| // }] | |
| // } | |
| if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) { | |
| // TODO: TYPE ts@4.1 may still infer it as Axis instead of Axis2D. Not sure if it's a bug | |
| const xAxis = coordSys.getAxis('x') as Axis2D; | |
| const yAxis = coordSys.getAxis('y') as Axis2D; | |
| const dims = coordSys.dimensions; | |
| if (isInfinity(data.get(dims[0], idx))) { | |
| point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]); | |
| } | |
| else if (isInfinity(data.get(dims[1], idx))) { | |
| point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]); | |
| } | |
| } | |
| // Use x, y if has any | |
| if (!isNaN(xPx)) { | |
| point[0] = xPx; | |
| } | |
| if (!isNaN(yPx)) { | |
| point[1] = yPx; | |
| } | |
| } | |
| data.setItemLayout(idx, point); | |
| } | |
| class MarkLineView extends MarkerView { | |
| static type = 'markLine'; | |
| type = MarkLineView.type; | |
| markerGroupMap: HashMap<LineDraw>; | |
| updateTransform(markLineModel: MarkLineModel, ecModel: GlobalModel, api: ExtensionAPI) { | |
| ecModel.eachSeries(function (seriesModel) { | |
| const mlModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markLine') as MarkLineModel; | |
| if (mlModel) { | |
| const mlData = mlModel.getData(); | |
| const fromData = inner(mlModel).from; | |
| const toData = inner(mlModel).to; | |
| // Update visual and layout of from symbol and to symbol | |
| fromData.each(function (idx) { | |
| updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api); | |
| updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api); | |
| }); | |
| // Update layout of line | |
| mlData.each(function (idx) { | |
| mlData.setItemLayout(idx, [ | |
| fromData.getItemLayout(idx), | |
| toData.getItemLayout(idx) | |
| ]); | |
| }); | |
| this.markerGroupMap.get(seriesModel.id).updateLayout(); | |
| } | |
| }, this); | |
| } | |
| renderSeries( | |
| seriesModel: SeriesModel, | |
| mlModel: MarkLineModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI | |
| ) { | |
| const coordSys = seriesModel.coordinateSystem; | |
| const seriesId = seriesModel.id; | |
| const seriesData = seriesModel.getData(); | |
| const lineDrawMap = this.markerGroupMap; | |
| const lineDraw = lineDrawMap.get(seriesId) | |
| || lineDrawMap.set(seriesId, new LineDraw()); | |
| this.group.add(lineDraw.group); | |
| const mlData = createList(coordSys, seriesModel, mlModel); | |
| const fromData = mlData.from; | |
| const toData = mlData.to; | |
| const lineData = mlData.line as SeriesData<MarkLineModel, LineDataVisual>; | |
| inner(mlModel).from = fromData; | |
| inner(mlModel).to = toData; | |
| // Line data for tooltip and formatter | |
| mlModel.setData(lineData); | |
| // TODO | |
| // Functionally, `symbolSize` & `symbolOffset` can also be 2D array now. | |
| // But the related logic and type definition are not finished yet. | |
| // Finish it if required | |
| let symbolType = mlModel.get('symbol'); | |
| let symbolSize = mlModel.get('symbolSize'); | |
| let symbolRotate = mlModel.get('symbolRotate'); | |
| let symbolOffset = mlModel.get('symbolOffset'); | |
| // TODO: support callback function like markPoint | |
| if (!isArray(symbolType)) { | |
| symbolType = [symbolType, symbolType]; | |
| } | |
| if (!isArray(symbolSize)) { | |
| symbolSize = [symbolSize, symbolSize]; | |
| } | |
| if (!isArray(symbolRotate)) { | |
| symbolRotate = [symbolRotate, symbolRotate]; | |
| } | |
| if (!isArray(symbolOffset)) { | |
| symbolOffset = [symbolOffset, symbolOffset]; | |
| } | |
| // Update visual and layout of from symbol and to symbol | |
| mlData.from.each(function (idx) { | |
| updateDataVisualAndLayout(fromData, idx, true); | |
| updateDataVisualAndLayout(toData, idx, false); | |
| }); | |
| // Update visual and layout of line | |
| lineData.each(function (idx) { | |
| const lineStyle = lineData.getItemModel<MarkLineMergedItemOption>(idx) | |
| .getModel('lineStyle').getLineStyle(); | |
| // lineData.setItemVisual(idx, { | |
| // color: lineColor || fromData.getItemVisual(idx, 'color') | |
| // }); | |
| lineData.setItemLayout(idx, [ | |
| fromData.getItemLayout(idx), | |
| toData.getItemLayout(idx) | |
| ]); | |
| if (lineStyle.stroke == null) { | |
| lineStyle.stroke = fromData.getItemVisual(idx, 'style').fill; | |
| } | |
| lineData.setItemVisual(idx, { | |
| fromSymbolKeepAspect: fromData.getItemVisual(idx, 'symbolKeepAspect'), | |
| fromSymbolOffset: fromData.getItemVisual(idx, 'symbolOffset'), | |
| fromSymbolRotate: fromData.getItemVisual(idx, 'symbolRotate'), | |
| fromSymbolSize: fromData.getItemVisual(idx, 'symbolSize') as number, | |
| fromSymbol: fromData.getItemVisual(idx, 'symbol'), | |
| toSymbolKeepAspect: toData.getItemVisual(idx, 'symbolKeepAspect'), | |
| toSymbolOffset: toData.getItemVisual(idx, 'symbolOffset'), | |
| toSymbolRotate: toData.getItemVisual(idx, 'symbolRotate'), | |
| toSymbolSize: toData.getItemVisual(idx, 'symbolSize') as number, | |
| toSymbol: toData.getItemVisual(idx, 'symbol'), | |
| style: lineStyle | |
| }); | |
| }); | |
| lineDraw.updateData(lineData); | |
| // Set host model for tooltip | |
| // FIXME | |
| mlData.line.eachItemGraphicEl(function (el) { | |
| getECData(el).dataModel = mlModel; | |
| el.traverse(function (child) { | |
| getECData(child).dataModel = mlModel; | |
| }); | |
| }); | |
| function updateDataVisualAndLayout( | |
| data: SeriesData<MarkLineModel>, | |
| idx: number, | |
| isFrom: boolean | |
| ) { | |
| const itemModel = data.getItemModel<MarkLineMergedItemOption>(idx); | |
| updateSingleMarkerEndLayout( | |
| data, idx, isFrom, seriesModel, api | |
| ); | |
| const style = itemModel.getModel('itemStyle').getItemStyle(); | |
| if (style.fill == null) { | |
| style.fill = getVisualFromData(seriesData, 'color') as ColorString; | |
| } | |
| data.setItemVisual(idx, { | |
| symbolKeepAspect: itemModel.get('symbolKeepAspect'), | |
| // `0` should be considered as a valid value, so use `retrieve2` instead of `||` | |
| symbolOffset: retrieve2( | |
| itemModel.get('symbolOffset', true), | |
| (symbolOffset as (string | number)[])[isFrom ? 0 : 1] | |
| ), | |
| symbolRotate: retrieve2( | |
| itemModel.get('symbolRotate', true), | |
| (symbolRotate as number[])[isFrom ? 0 : 1] | |
| ), | |
| // TODO: when 2d array is supported, it should ignore parent | |
| symbolSize: retrieve2( | |
| itemModel.get('symbolSize'), | |
| (symbolSize as number[])[isFrom ? 0 : 1] | |
| ), | |
| symbol: retrieve2( | |
| itemModel.get('symbol', true), | |
| (symbolType as string[])[isFrom ? 0 : 1] | |
| ), | |
| style | |
| }); | |
| } | |
| this.markKeep(lineDraw); | |
| lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent'); | |
| } | |
| } | |
| function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlModel: MarkLineModel) { | |
| let coordDimsInfos: SeriesDimensionDefine[]; | |
| if (coordSys) { | |
| coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { | |
| const info = seriesModel.getData().getDimensionInfo( | |
| seriesModel.getData().mapDimension(coordDim) | |
| ) || {}; | |
| // In map series data don't have lng and lat dimension. Fallback to same with coordSys | |
| return extend(extend({}, info), { | |
| name: coordDim, | |
| // DON'T use ordinalMeta to parse and collect ordinal. | |
| ordinalMeta: null | |
| }); | |
| }); | |
| } | |
| else { | |
| coordDimsInfos = [{ | |
| name: 'value', | |
| type: 'float' | |
| }]; | |
| } | |
| const fromData = new SeriesData(coordDimsInfos, mlModel); | |
| const toData = new SeriesData(coordDimsInfos, mlModel); | |
| // No dimensions | |
| const lineData = new SeriesData([], mlModel); | |
| let optData = map(mlModel.get('data'), curry( | |
| markLineTransform, seriesModel, coordSys, mlModel | |
| )); | |
| if (coordSys) { | |
| optData = filter( | |
| optData, curry(markLineFilter, coordSys) | |
| ); | |
| } | |
| const dimValueGetter = markerHelper.createMarkerDimValueGetter(!!coordSys, coordDimsInfos); | |
| fromData.initData( | |
| map(optData, function (item) { | |
| return item[0]; | |
| }), | |
| null, | |
| dimValueGetter | |
| ); | |
| toData.initData( | |
| map(optData, function (item) { | |
| return item[1]; | |
| }), | |
| null, | |
| dimValueGetter | |
| ); | |
| lineData.initData( | |
| map(optData, function (item) { | |
| return item[2]; | |
| }) | |
| ); | |
| lineData.hasItemOption = true; | |
| return { | |
| from: fromData, | |
| to: toData, | |
| line: lineData | |
| }; | |
| } | |
| export default MarkLineView; | |