blob: 140509dd3902e791916552802ded6c77d422da9b [file] [log] [blame]
/*
* 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 { isArray, each } from 'zrender/src/core/util';
import * as vector from 'zrender/src/core/vector';
import * as symbolUtil from '../../util/symbol';
import ECLinePath from './LinePath';
import * as graphic from '../../util/graphic';
import { enableHoverEmphasis, enterEmphasis, leaveEmphasis, SPECIAL_STATES } from '../../util/states';
import {getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
import {round} from '../../util/number';
import List from '../../data/List';
import { ZRTextAlign, ZRTextVerticalAlign, LineLabelOption, ColorString } from '../../util/types';
import SeriesModel from '../../model/Series';
import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw';
import { TextStyleProps } from 'zrender/src/graphic/Text';
import { LineDataVisual } from '../../visual/commonVisualTypes';
import Model from '../../model/Model';
const SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol'] as const;
type ECSymbol = ReturnType<typeof createSymbol>;
type LineECSymbol = ECSymbol & {
__specifiedRotation: number
};
type LineList = List<SeriesModel, LineDataVisual>;
export interface LineLabel extends graphic.Text {
lineLabelOriginalOpacity: number
}
interface InnerLineLabel extends LineLabel {
__align: TextStyleProps['align']
__verticalAlign: TextStyleProps['verticalAlign']
__position: LineLabelOption['position']
__labelDistance: number[]
}
function makeSymbolTypeKey(symbolCategory: 'fromSymbol' | 'toSymbol') {
return '_' + symbolCategory + 'Type' as '_fromSymbolType' | '_toSymbolType';
}
/**
* @inner
*/
function createSymbol(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: number) {
const symbolType = lineData.getItemVisual(idx, name);
if (!symbolType || symbolType === 'none') {
return;
}
const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize');
const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate');
const symbolOffset = lineData.getItemVisual(idx, name + 'Offset' as 'fromSymbolOffset' | 'toSymbolOffset');
const symbolKeepAspect = lineData.getItemVisual(idx,
name + 'KeepAspect' as 'fromSymbolKeepAspect' | 'toSymbolKeepAspect');
const symbolSizeArr = symbolUtil.normalizeSymbolSize(symbolSize);
const symbolOffsetArr = symbolUtil.normalizeSymbolOffset(symbolOffset || 0, symbolSizeArr);
const symbolPath = symbolUtil.createSymbol(
symbolType,
-symbolSizeArr[0] / 2 + (symbolOffsetArr as number[])[0],
-symbolSizeArr[1] / 2 + (symbolOffsetArr as number[])[1],
symbolSizeArr[0],
symbolSizeArr[1],
null,
symbolKeepAspect
);
(symbolPath as LineECSymbol).__specifiedRotation = symbolRotate == null || isNaN(symbolRotate)
? void 0
: +symbolRotate * Math.PI / 180 || 0;
symbolPath.name = name;
return symbolPath;
}
function createLine(points: number[][]) {
const line = new ECLinePath({
name: 'line',
subPixelOptimize: true
});
setLinePoints(line.shape, points);
return line;
}
function setLinePoints(targetShape: ECLinePath['shape'], points: number[][]) {
type CurveShape = ECLinePath['shape'] & {
cpx1: number
cpy1: number
};
targetShape.x1 = points[0][0];
targetShape.y1 = points[0][1];
targetShape.x2 = points[1][0];
targetShape.y2 = points[1][1];
targetShape.percent = 1;
const cp1 = points[2];
if (cp1) {
(targetShape as CurveShape).cpx1 = cp1[0];
(targetShape as CurveShape).cpy1 = cp1[1];
}
else {
(targetShape as CurveShape).cpx1 = NaN;
(targetShape as CurveShape).cpy1 = NaN;
}
}
class Line extends graphic.Group {
private _fromSymbolType: string;
private _toSymbolType: string;
constructor(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
super();
this._createLine(lineData as LineList, idx, seriesScope);
}
_createLine(lineData: LineList, idx: number, seriesScope?: LineDrawSeriesScope) {
const seriesModel = lineData.hostModel;
const linePoints = lineData.getItemLayout(idx);
const line = createLine(linePoints);
line.shape.percent = 0;
graphic.initProps(line, {
shape: {
percent: 1
}
}, seriesModel, idx);
this.add(line);
each(SYMBOL_CATEGORIES, function (symbolCategory) {
const symbol = createSymbol(symbolCategory, lineData, idx);
// symbols must added after line to make sure
// it will be updated after line#update.
// Or symbol position and rotation update in line#beforeUpdate will be one frame slow
this.add(symbol);
this[makeSymbolTypeKey(symbolCategory)] = lineData.getItemVisual(idx, symbolCategory);
}, this);
this._updateCommonStl(lineData, idx, seriesScope);
}
// TODO More strict on the List type in parameters?
updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) {
const seriesModel = lineData.hostModel;
const line = this.childOfName('line') as ECLinePath;
const linePoints = lineData.getItemLayout(idx);
const target = {
shape: {} as ECLinePath['shape']
};
setLinePoints(target.shape, linePoints);
graphic.updateProps(line, target, seriesModel, idx);
each(SYMBOL_CATEGORIES, function (symbolCategory) {
const symbolType = (lineData as LineList).getItemVisual(idx, symbolCategory);
const key = makeSymbolTypeKey(symbolCategory);
// Symbol changed
if (this[key] !== symbolType) {
this.remove(this.childOfName(symbolCategory));
const symbol = createSymbol(symbolCategory, lineData as LineList, idx);
this.add(symbol);
}
this[key] = symbolType;
}, this);
this._updateCommonStl(lineData, idx, seriesScope);
};
getLinePath() {
return this.childAt(0) as graphic.Line;
}
_updateCommonStl(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
const seriesModel = lineData.hostModel as SeriesModel;
const line = this.childOfName('line') as ECLinePath;
let emphasisLineStyle = seriesScope && seriesScope.emphasisLineStyle;
let blurLineStyle = seriesScope && seriesScope.blurLineStyle;
let selectLineStyle = seriesScope && seriesScope.selectLineStyle;
let labelStatesModels = seriesScope && seriesScope.labelStatesModels;
// Optimization for large dataset
if (!seriesScope || lineData.hasItemOption) {
const itemModel = lineData.getItemModel<LineDrawModelOption>(idx);
emphasisLineStyle = itemModel.getModel(['emphasis', 'lineStyle']).getLineStyle();
blurLineStyle = itemModel.getModel(['blur', 'lineStyle']).getLineStyle();
selectLineStyle = itemModel.getModel(['select', 'lineStyle']).getLineStyle();
labelStatesModels = getLabelStatesModels(itemModel);
}
const lineStyle = lineData.getItemVisual(idx, 'style');
const visualColor = lineStyle.stroke;
line.useStyle(lineStyle);
line.style.fill = null;
line.style.strokeNoScale = true;
line.ensureState('emphasis').style = emphasisLineStyle;
line.ensureState('blur').style = blurLineStyle;
line.ensureState('select').style = selectLineStyle;
// Update symbol
each(SYMBOL_CATEGORIES, function (symbolCategory) {
const symbol = this.childOfName(symbolCategory) as ECSymbol;
if (symbol) {
// Share opacity and color with line.
symbol.setColor(visualColor);
symbol.style.opacity = lineStyle.opacity;
for (let i = 0; i < SPECIAL_STATES.length; i++) {
const stateName = SPECIAL_STATES[i];
const lineState = line.getState(stateName);
if (lineState) {
const lineStateStyle = lineState.style || {};
const state = symbol.ensureState(stateName);
const stateStyle = state.style || (state.style = {});
if (lineStateStyle.stroke != null) {
stateStyle[symbol.__isEmptyBrush ? 'stroke' : 'fill'] = lineStateStyle.stroke;
}
if (lineStateStyle.opacity != null) {
stateStyle.opacity = lineStateStyle.opacity;
}
}
}
symbol.markRedraw();
}
}, this);
const rawVal = seriesModel.getRawValue(idx) as number;
setLabelStyle(this, labelStatesModels, {
labelDataIndex: idx,
labelFetcher: {
getFormattedLabel(dataIndex, stateName) {
return seriesModel.getFormattedLabel(dataIndex, stateName, lineData.dataType);
}
},
inheritColor: visualColor as ColorString || '#000',
defaultOpacity: lineStyle.opacity,
defaultText: (rawVal == null
? lineData.getName(idx)
: isFinite(rawVal)
? round(rawVal)
: rawVal) + ''
});
const label = this.getTextContent() as InnerLineLabel;
// Always set `textStyle` even if `normalStyle.text` is null, because default
// values have to be set on `normalStyle`.
if (label) {
const labelNormalModel = labelStatesModels.normal as unknown as Model<LineLabelOption>;
label.__align = label.style.align;
label.__verticalAlign = label.style.verticalAlign;
// 'start', 'middle', 'end'
label.__position = labelNormalModel.get('position') || 'middle';
let distance = labelNormalModel.get('distance');
if (!isArray(distance)) {
distance = [distance, distance];
}
label.__labelDistance = distance;
}
this.setTextConfig({
position: null,
local: true,
inside: false // Can't be inside for stroke element.
});
enableHoverEmphasis(this);
}
highlight() {
enterEmphasis(this);
}
downplay() {
leaveEmphasis(this);
}
updateLayout(lineData: List, idx: number) {
this.setLinePoints(lineData.getItemLayout(idx));
}
setLinePoints(points: number[][]) {
const linePath = this.childOfName('line') as ECLinePath;
setLinePoints(linePath.shape, points);
linePath.dirty();
}
beforeUpdate() {
const lineGroup = this;
const symbolFrom = lineGroup.childOfName('fromSymbol') as ECSymbol;
const symbolTo = lineGroup.childOfName('toSymbol') as ECSymbol;
const label = lineGroup.getTextContent() as InnerLineLabel;
// Quick reject
if (!symbolFrom && !symbolTo && (!label || label.ignore)) {
return;
}
let invScale = 1;
let parentNode = this.parent;
while (parentNode) {
if (parentNode.scaleX) {
invScale /= parentNode.scaleX;
}
parentNode = parentNode.parent;
}
const line = lineGroup.childOfName('line') as ECLinePath;
// If line not changed
// FIXME Parent scale changed
if (!this.__dirty && !line.__dirty) {
return;
}
const percent = line.shape.percent;
const fromPos = line.pointAt(0);
const toPos = line.pointAt(percent);
const d = vector.sub([], toPos, fromPos);
vector.normalize(d, d);
function setSymbolRotation(symbol: ECSymbol, percent: 0 | 1) {
// Fix #12388
// when symbol is set to be 'arrow' in markLine,
// symbolRotate value will be ignored, and compulsively use tangent angle.
// rotate by default if symbol rotation is not specified
const specifiedRotation = (symbol as LineECSymbol).__specifiedRotation;
if (specifiedRotation == null) {
const tangent = line.tangentAt(percent);
symbol.attr('rotation', (percent === 1 ? -1 : 1) * Math.PI / 2 - Math.atan2(
tangent[1], tangent[0]
));
}
else {
symbol.attr('rotation', specifiedRotation);
}
}
if (symbolFrom) {
symbolFrom.setPosition(fromPos);
setSymbolRotation(symbolFrom, 0);
symbolFrom.scaleX = symbolFrom.scaleY = invScale * percent;
symbolFrom.markRedraw();
}
if (symbolTo) {
symbolTo.setPosition(toPos);
setSymbolRotation(symbolTo, 1);
symbolTo.scaleX = symbolTo.scaleY = invScale * percent;
symbolTo.markRedraw();
}
if (label && !label.ignore) {
label.x = label.y = 0;
label.originX = label.originY = 0;
let textAlign: ZRTextAlign;
let textVerticalAlign: ZRTextVerticalAlign;
const distance = label.__labelDistance;
const distanceX = distance[0] * invScale;
const distanceY = distance[1] * invScale;
const halfPercent = percent / 2;
const tangent = line.tangentAt(halfPercent);
const n = [tangent[1], -tangent[0]];
const cp = line.pointAt(halfPercent);
if (n[1] > 0) {
n[0] = -n[0];
n[1] = -n[1];
}
const dir = tangent[0] < 0 ? -1 : 1;
if (label.__position !== 'start' && label.__position !== 'end') {
let rotation = -Math.atan2(tangent[1], tangent[0]);
if (toPos[0] < fromPos[0]) {
rotation = Math.PI + rotation;
}
label.rotation = rotation;
}
let dy;
switch (label.__position) {
case 'insideStartTop':
case 'insideMiddleTop':
case 'insideEndTop':
case 'middle':
dy = -distanceY;
textVerticalAlign = 'bottom';
break;
case 'insideStartBottom':
case 'insideMiddleBottom':
case 'insideEndBottom':
dy = distanceY;
textVerticalAlign = 'top';
break;
default:
dy = 0;
textVerticalAlign = 'middle';
}
switch (label.__position) {
case 'end':
label.x = d[0] * distanceX + toPos[0];
label.y = d[1] * distanceY + toPos[1];
textAlign = d[0] > 0.8 ? 'left' : (d[0] < -0.8 ? 'right' : 'center');
textVerticalAlign = d[1] > 0.8 ? 'top' : (d[1] < -0.8 ? 'bottom' : 'middle');
break;
case 'start':
label.x = -d[0] * distanceX + fromPos[0];
label.y = -d[1] * distanceY + fromPos[1];
textAlign = d[0] > 0.8 ? 'right' : (d[0] < -0.8 ? 'left' : 'center');
textVerticalAlign = d[1] > 0.8 ? 'bottom' : (d[1] < -0.8 ? 'top' : 'middle');
break;
case 'insideStartTop':
case 'insideStart':
case 'insideStartBottom':
label.x = distanceX * dir + fromPos[0];
label.y = fromPos[1] + dy;
textAlign = tangent[0] < 0 ? 'right' : 'left';
label.originX = -distanceX * dir;
label.originY = -dy;
break;
case 'insideMiddleTop':
case 'insideMiddle':
case 'insideMiddleBottom':
case 'middle':
label.x = cp[0];
label.y = cp[1] + dy;
textAlign = 'center';
label.originY = -dy;
break;
case 'insideEndTop':
case 'insideEnd':
case 'insideEndBottom':
label.x = -distanceX * dir + toPos[0];
label.y = toPos[1] + dy;
textAlign = tangent[0] >= 0 ? 'right' : 'left';
label.originX = distanceX * dir;
label.originY = -dy;
break;
}
label.scaleX = label.scaleY = invScale;
label.setStyle({
// Use the user specified text align and baseline first
verticalAlign: label.__verticalAlign || textVerticalAlign,
align: label.__align || textAlign
});
}
}
}
export default Line;