Current File : //home/tradevaly/www/node_modules/echarts/src/chart/line/LineView.js |
/*
* 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.
*/
// FIXME step not support polar
import {__DEV__} from '../../config';
import * as zrUtil from 'zrender/src/core/util';
import {fromPoints} from 'zrender/src/core/bbox';
import SymbolDraw from '../helper/SymbolDraw';
import SymbolClz from '../helper/Symbol';
import lineAnimationDiff from './lineAnimationDiff';
import * as graphic from '../../util/graphic';
import * as modelUtil from '../../util/model';
import {Polyline, Polygon} from './poly';
import ChartView from '../../view/Chart';
import {prepareDataCoordInfo, getStackedOnPoint} from './helper';
import {createGridClipPath, createPolarClipPath} from '../helper/createClipPathFromCoordSys';
function isPointsSame(points1, points2) {
if (points1.length !== points2.length) {
return;
}
for (var i = 0; i < points1.length; i++) {
var p1 = points1[i];
var p2 = points2[i];
if (p1[0] !== p2[0] || p1[1] !== p2[1]) {
return;
}
}
return true;
}
function getBoundingDiff(points1, points2) {
var min1 = [];
var max1 = [];
var min2 = [];
var max2 = [];
fromPoints(points1, min1, max1);
fromPoints(points2, min2, max2);
// Get a max value from each corner of two boundings.
return Math.max(
Math.abs(min1[0] - min2[0]),
Math.abs(min1[1] - min2[1]),
Math.abs(max1[0] - max2[0]),
Math.abs(max1[1] - max2[1])
);
}
function getSmooth(smooth) {
return typeof (smooth) === 'number' ? smooth : (smooth ? 0.5 : 0);
}
/**
* @param {module:echarts/coord/cartesian/Cartesian2D|module:echarts/coord/polar/Polar} coordSys
* @param {module:echarts/data/List} data
* @param {Object} dataCoordInfo
* @param {Array.<Array.<number>>} points
*/
function getStackedOnPoints(coordSys, data, dataCoordInfo) {
if (!dataCoordInfo.valueDim) {
return [];
}
var points = [];
for (var idx = 0, len = data.count(); idx < len; idx++) {
points.push(getStackedOnPoint(dataCoordInfo, coordSys, data, idx));
}
return points;
}
function turnPointsIntoStep(points, coordSys, stepTurnAt) {
var baseAxis = coordSys.getBaseAxis();
var baseIndex = baseAxis.dim === 'x' || baseAxis.dim === 'radius' ? 0 : 1;
var stepPoints = [];
for (var i = 0; i < points.length - 1; i++) {
var nextPt = points[i + 1];
var pt = points[i];
stepPoints.push(pt);
var stepPt = [];
switch (stepTurnAt) {
case 'end':
stepPt[baseIndex] = nextPt[baseIndex];
stepPt[1 - baseIndex] = pt[1 - baseIndex];
// default is start
stepPoints.push(stepPt);
break;
case 'middle':
// default is start
var middle = (pt[baseIndex] + nextPt[baseIndex]) / 2;
var stepPt2 = [];
stepPt[baseIndex] = stepPt2[baseIndex] = middle;
stepPt[1 - baseIndex] = pt[1 - baseIndex];
stepPt2[1 - baseIndex] = nextPt[1 - baseIndex];
stepPoints.push(stepPt);
stepPoints.push(stepPt2);
break;
default:
stepPt[baseIndex] = pt[baseIndex];
stepPt[1 - baseIndex] = nextPt[1 - baseIndex];
// default is start
stepPoints.push(stepPt);
}
}
// Last points
points[i] && stepPoints.push(points[i]);
return stepPoints;
}
function getVisualGradient(data, coordSys) {
var visualMetaList = data.getVisual('visualMeta');
if (!visualMetaList || !visualMetaList.length || !data.count()) {
// When data.count() is 0, gradient range can not be calculated.
return;
}
if (coordSys.type !== 'cartesian2d') {
if (__DEV__) {
console.warn('Visual map on line style is only supported on cartesian2d.');
}
return;
}
var coordDim;
var visualMeta;
for (var i = visualMetaList.length - 1; i >= 0; i--) {
var dimIndex = visualMetaList[i].dimension;
var dimName = data.dimensions[dimIndex];
var dimInfo = data.getDimensionInfo(dimName);
coordDim = dimInfo && dimInfo.coordDim;
// Can only be x or y
if (coordDim === 'x' || coordDim === 'y') {
visualMeta = visualMetaList[i];
break;
}
}
if (!visualMeta) {
if (__DEV__) {
console.warn('Visual map on line style only support x or y dimension.');
}
return;
}
// If the area to be rendered is bigger than area defined by LinearGradient,
// the canvas spec prescribes that the color of the first stop and the last
// stop should be used. But if two stops are added at offset 0, in effect
// browsers use the color of the second stop to render area outside
// LinearGradient. So we can only infinitesimally extend area defined in
// LinearGradient to render `outerColors`.
var axis = coordSys.getAxis(coordDim);
// dataToCoor mapping may not be linear, but must be monotonic.
var colorStops = zrUtil.map(visualMeta.stops, function (stop) {
return {
coord: axis.toGlobalCoord(axis.dataToCoord(stop.value)),
color: stop.color
};
});
var stopLen = colorStops.length;
var outerColors = visualMeta.outerColors.slice();
if (stopLen && colorStops[0].coord > colorStops[stopLen - 1].coord) {
colorStops.reverse();
outerColors.reverse();
}
var tinyExtent = 10; // Arbitrary value: 10px
var minCoord = colorStops[0].coord - tinyExtent;
var maxCoord = colorStops[stopLen - 1].coord + tinyExtent;
var coordSpan = maxCoord - minCoord;
if (coordSpan < 1e-3) {
return 'transparent';
}
zrUtil.each(colorStops, function (stop) {
stop.offset = (stop.coord - minCoord) / coordSpan;
});
colorStops.push({
offset: stopLen ? colorStops[stopLen - 1].offset : 0.5,
color: outerColors[1] || 'transparent'
});
colorStops.unshift({ // notice colorStops.length have been changed.
offset: stopLen ? colorStops[0].offset : 0.5,
color: outerColors[0] || 'transparent'
});
// zrUtil.each(colorStops, function (colorStop) {
// // Make sure each offset has rounded px to avoid not sharp edge
// colorStop.offset = (Math.round(colorStop.offset * (end - start) + start) - start) / (end - start);
// });
var gradient = new graphic.LinearGradient(0, 0, 0, 0, colorStops, true);
gradient[coordDim] = minCoord;
gradient[coordDim + '2'] = maxCoord;
return gradient;
}
function getIsIgnoreFunc(seriesModel, data, coordSys) {
var showAllSymbol = seriesModel.get('showAllSymbol');
var isAuto = showAllSymbol === 'auto';
if (showAllSymbol && !isAuto) {
return;
}
var categoryAxis = coordSys.getAxesByScale('ordinal')[0];
if (!categoryAxis) {
return;
}
// Note that category label interval strategy might bring some weird effect
// in some scenario: users may wonder why some of the symbols are not
// displayed. So we show all symbols as possible as we can.
if (isAuto
// Simplify the logic, do not determine label overlap here.
&& canShowAllSymbolForCategory(categoryAxis, data)
) {
return;
}
// Otherwise follow the label interval strategy on category axis.
var categoryDataDim = data.mapDimension(categoryAxis.dim);
var labelMap = {};
zrUtil.each(categoryAxis.getViewLabels(), function (labelItem) {
labelMap[labelItem.tickValue] = 1;
});
return function (dataIndex) {
return !labelMap.hasOwnProperty(data.get(categoryDataDim, dataIndex));
};
}
function canShowAllSymbolForCategory(categoryAxis, data) {
// In mose cases, line is monotonous on category axis, and the label size
// is close with each other. So we check the symbol size and some of the
// label size alone with the category axis to estimate whether all symbol
// can be shown without overlap.
var axisExtent = categoryAxis.getExtent();
var availSize = Math.abs(axisExtent[1] - axisExtent[0]) / categoryAxis.scale.count();
isNaN(availSize) && (availSize = 0); // 0/0 is NaN.
// Sampling some points, max 5.
var dataLen = data.count();
var step = Math.max(1, Math.round(dataLen / 5));
for (var dataIndex = 0; dataIndex < dataLen; dataIndex += step) {
if (SymbolClz.getSymbolSize(
data, dataIndex
// Only for cartesian, where `isHorizontal` exists.
)[categoryAxis.isHorizontal() ? 1 : 0]
// Empirical number
* 1.5 > availSize
) {
return false;
}
}
return true;
}
function createLineClipPath(coordSys, hasAnimation, seriesModel) {
if (coordSys.type === 'cartesian2d') {
var isHorizontal = coordSys.getBaseAxis().isHorizontal();
var clipPath = createGridClipPath(coordSys, hasAnimation, seriesModel);
// Expand clip shape to avoid clipping when line value exceeds axis
if (!seriesModel.get('clip', true)) {
var rectShape = clipPath.shape;
var expandSize = Math.max(rectShape.width, rectShape.height);
if (isHorizontal) {
rectShape.y -= expandSize;
rectShape.height += expandSize * 2;
}
else {
rectShape.x -= expandSize;
rectShape.width += expandSize * 2;
}
}
return clipPath;
}
else {
return createPolarClipPath(coordSys, hasAnimation, seriesModel);
}
}
export default ChartView.extend({
type: 'line',
init: function () {
var lineGroup = new graphic.Group();
var symbolDraw = new SymbolDraw();
this.group.add(symbolDraw.group);
this._symbolDraw = symbolDraw;
this._lineGroup = lineGroup;
},
render: function (seriesModel, ecModel, api) {
var coordSys = seriesModel.coordinateSystem;
var group = this.group;
var data = seriesModel.getData();
var lineStyleModel = seriesModel.getModel('lineStyle');
var areaStyleModel = seriesModel.getModel('areaStyle');
var points = data.mapArray(data.getItemLayout);
var isCoordSysPolar = coordSys.type === 'polar';
var prevCoordSys = this._coordSys;
var symbolDraw = this._symbolDraw;
var polyline = this._polyline;
var polygon = this._polygon;
var lineGroup = this._lineGroup;
var hasAnimation = seriesModel.get('animation');
var isAreaChart = !areaStyleModel.isEmpty();
var valueOrigin = areaStyleModel.get('origin');
var dataCoordInfo = prepareDataCoordInfo(coordSys, data, valueOrigin);
var stackedOnPoints = getStackedOnPoints(coordSys, data, dataCoordInfo);
var showSymbol = seriesModel.get('showSymbol');
var isIgnoreFunc = showSymbol && !isCoordSysPolar
&& getIsIgnoreFunc(seriesModel, data, coordSys);
// Remove temporary symbols
var oldData = this._data;
oldData && oldData.eachItemGraphicEl(function (el, idx) {
if (el.__temp) {
group.remove(el);
oldData.setItemGraphicEl(idx, null);
}
});
// Remove previous created symbols if showSymbol changed to false
if (!showSymbol) {
symbolDraw.remove();
}
group.add(lineGroup);
// FIXME step not support polar
var step = !isCoordSysPolar && seriesModel.get('step');
var clipShapeForSymbol;
if (coordSys && coordSys.getArea && seriesModel.get('clip', true)) {
clipShapeForSymbol = coordSys.getArea();
// Avoid float number rounding error for symbol on the edge of axis extent.
// See #7913 and `test/dataZoom-clip.html`.
if (clipShapeForSymbol.width != null) {
clipShapeForSymbol.x -= 0.1;
clipShapeForSymbol.y -= 0.1;
clipShapeForSymbol.width += 0.2;
clipShapeForSymbol.height += 0.2;
}
else if (clipShapeForSymbol.r0) {
clipShapeForSymbol.r0 -= 0.5;
clipShapeForSymbol.r1 += 0.5;
}
}
this._clipShapeForSymbol = clipShapeForSymbol;
// Initialization animation or coordinate system changed
if (
!(polyline && prevCoordSys.type === coordSys.type && step === this._step)
) {
showSymbol && symbolDraw.updateData(data, {
isIgnore: isIgnoreFunc,
clipShape: clipShapeForSymbol
});
if (step) {
// TODO If stacked series is not step
points = turnPointsIntoStep(points, coordSys, step);
stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step);
}
polyline = this._newPolyline(points, coordSys, hasAnimation);
if (isAreaChart) {
polygon = this._newPolygon(
points, stackedOnPoints,
coordSys, hasAnimation
);
}
lineGroup.setClipPath(createLineClipPath(coordSys, true, seriesModel));
}
else {
if (isAreaChart && !polygon) {
// If areaStyle is added
polygon = this._newPolygon(
points, stackedOnPoints,
coordSys, hasAnimation
);
}
else if (polygon && !isAreaChart) {
// If areaStyle is removed
lineGroup.remove(polygon);
polygon = this._polygon = null;
}
// Update clipPath
lineGroup.setClipPath(createLineClipPath(coordSys, false, seriesModel));
// Always update, or it is wrong in the case turning on legend
// because points are not changed
showSymbol && symbolDraw.updateData(data, {
isIgnore: isIgnoreFunc,
clipShape: clipShapeForSymbol
});
// Stop symbol animation and sync with line points
// FIXME performance?
data.eachItemGraphicEl(function (el) {
el.stopAnimation(true);
});
// In the case data zoom triggerred refreshing frequently
// Data may not change if line has a category axis. So it should animate nothing
if (!isPointsSame(this._stackedOnPoints, stackedOnPoints)
|| !isPointsSame(this._points, points)
) {
if (hasAnimation) {
this._updateAnimation(
data, stackedOnPoints, coordSys, api, step, valueOrigin
);
}
else {
// Not do it in update with animation
if (step) {
// TODO If stacked series is not step
points = turnPointsIntoStep(points, coordSys, step);
stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step);
}
polyline.setShape({
points: points
});
polygon && polygon.setShape({
points: points,
stackedOnPoints: stackedOnPoints
});
}
}
}
var visualColor = getVisualGradient(data, coordSys) || data.getVisual('color');
polyline.useStyle(zrUtil.defaults(
// Use color in lineStyle first
lineStyleModel.getLineStyle(),
{
fill: 'none',
stroke: visualColor,
lineJoin: 'bevel'
}
));
var smooth = seriesModel.get('smooth');
smooth = getSmooth(seriesModel.get('smooth'));
polyline.setShape({
smooth: smooth,
smoothMonotone: seriesModel.get('smoothMonotone'),
connectNulls: seriesModel.get('connectNulls')
});
if (polygon) {
var stackedOnSeries = data.getCalculationInfo('stackedOnSeries');
var stackedOnSmooth = 0;
polygon.useStyle(zrUtil.defaults(
areaStyleModel.getAreaStyle(),
{
fill: visualColor,
opacity: 0.7,
lineJoin: 'bevel'
}
));
if (stackedOnSeries) {
stackedOnSmooth = getSmooth(stackedOnSeries.get('smooth'));
}
polygon.setShape({
smooth: smooth,
stackedOnSmooth: stackedOnSmooth,
smoothMonotone: seriesModel.get('smoothMonotone'),
connectNulls: seriesModel.get('connectNulls')
});
}
this._data = data;
// Save the coordinate system for transition animation when data changed
this._coordSys = coordSys;
this._stackedOnPoints = stackedOnPoints;
this._points = points;
this._step = step;
this._valueOrigin = valueOrigin;
},
dispose: function () {},
highlight: function (seriesModel, ecModel, api, payload) {
var data = seriesModel.getData();
var dataIndex = modelUtil.queryDataIndex(data, payload);
if (!(dataIndex instanceof Array) && dataIndex != null && dataIndex >= 0) {
var symbol = data.getItemGraphicEl(dataIndex);
if (!symbol) {
// Create a temporary symbol if it is not exists
var pt = data.getItemLayout(dataIndex);
if (!pt) {
// Null data
return;
}
// fix #11360: should't draw symbol outside clipShapeForSymbol
if (this._clipShapeForSymbol && !this._clipShapeForSymbol.contain(pt[0], pt[1])) {
return;
}
symbol = new SymbolClz(data, dataIndex);
symbol.position = pt;
symbol.setZ(
seriesModel.get('zlevel'),
seriesModel.get('z')
);
symbol.ignore = isNaN(pt[0]) || isNaN(pt[1]);
symbol.__temp = true;
data.setItemGraphicEl(dataIndex, symbol);
// Stop scale animation
symbol.stopSymbolAnimation(true);
this.group.add(symbol);
}
symbol.highlight();
}
else {
// Highlight whole series
ChartView.prototype.highlight.call(
this, seriesModel, ecModel, api, payload
);
}
},
downplay: function (seriesModel, ecModel, api, payload) {
var data = seriesModel.getData();
var dataIndex = modelUtil.queryDataIndex(data, payload);
if (dataIndex != null && dataIndex >= 0) {
var symbol = data.getItemGraphicEl(dataIndex);
if (symbol) {
if (symbol.__temp) {
data.setItemGraphicEl(dataIndex, null);
this.group.remove(symbol);
}
else {
symbol.downplay();
}
}
}
else {
// FIXME
// can not downplay completely.
// Downplay whole series
ChartView.prototype.downplay.call(
this, seriesModel, ecModel, api, payload
);
}
},
/**
* @param {module:zrender/container/Group} group
* @param {Array.<Array.<number>>} points
* @private
*/
_newPolyline: function (points) {
var polyline = this._polyline;
// Remove previous created polyline
if (polyline) {
this._lineGroup.remove(polyline);
}
polyline = new Polyline({
shape: {
points: points
},
silent: true,
z2: 10
});
this._lineGroup.add(polyline);
this._polyline = polyline;
return polyline;
},
/**
* @param {module:zrender/container/Group} group
* @param {Array.<Array.<number>>} stackedOnPoints
* @param {Array.<Array.<number>>} points
* @private
*/
_newPolygon: function (points, stackedOnPoints) {
var polygon = this._polygon;
// Remove previous created polygon
if (polygon) {
this._lineGroup.remove(polygon);
}
polygon = new Polygon({
shape: {
points: points,
stackedOnPoints: stackedOnPoints
},
silent: true
});
this._lineGroup.add(polygon);
this._polygon = polygon;
return polygon;
},
/**
* @private
*/
// FIXME Two value axis
_updateAnimation: function (data, stackedOnPoints, coordSys, api, step, valueOrigin) {
var polyline = this._polyline;
var polygon = this._polygon;
var seriesModel = data.hostModel;
var diff = lineAnimationDiff(
this._data, data,
this._stackedOnPoints, stackedOnPoints,
this._coordSys, coordSys,
this._valueOrigin, valueOrigin
);
var current = diff.current;
var stackedOnCurrent = diff.stackedOnCurrent;
var next = diff.next;
var stackedOnNext = diff.stackedOnNext;
if (step) {
// TODO If stacked series is not step
current = turnPointsIntoStep(diff.current, coordSys, step);
stackedOnCurrent = turnPointsIntoStep(diff.stackedOnCurrent, coordSys, step);
next = turnPointsIntoStep(diff.next, coordSys, step);
stackedOnNext = turnPointsIntoStep(diff.stackedOnNext, coordSys, step);
}
// Don't apply animation if diff is large.
// For better result and avoid memory explosion problems like
// https://github.com/apache/incubator-echarts/issues/12229
if (getBoundingDiff(current, next) > 3000
|| (polygon && getBoundingDiff(stackedOnCurrent, stackedOnNext) > 3000)
) {
polyline.setShape({
points: next
});
if (polygon) {
polygon.setShape({
points: next,
stackedOnPoints: stackedOnNext
});
}
return;
}
// `diff.current` is subset of `current` (which should be ensured by
// turnPointsIntoStep), so points in `__points` can be updated when
// points in `current` are update during animation.
polyline.shape.__points = diff.current;
polyline.shape.points = current;
graphic.updateProps(polyline, {
shape: {
points: next
}
}, seriesModel);
if (polygon) {
polygon.setShape({
points: current,
stackedOnPoints: stackedOnCurrent
});
graphic.updateProps(polygon, {
shape: {
points: next,
stackedOnPoints: stackedOnNext
}
}, seriesModel);
}
var updatedDataInfo = [];
var diffStatus = diff.status;
for (var i = 0; i < diffStatus.length; i++) {
var cmd = diffStatus[i].cmd;
if (cmd === '=') {
var el = data.getItemGraphicEl(diffStatus[i].idx1);
if (el) {
updatedDataInfo.push({
el: el,
ptIdx: i // Index of points
});
}
}
}
if (polyline.animators && polyline.animators.length) {
polyline.animators[0].during(function () {
for (var i = 0; i < updatedDataInfo.length; i++) {
var el = updatedDataInfo[i].el;
el.attr('position', polyline.shape.__points[updatedDataInfo[i].ptIdx]);
}
});
}
},
remove: function (ecModel) {
var group = this.group;
var oldData = this._data;
this._lineGroup.removeAll();
this._symbolDraw.remove(true);
// Remove temporary created elements when highlighting
oldData && oldData.eachItemGraphicEl(function (el, idx) {
if (el.__temp) {
group.remove(el);
oldData.setItemGraphicEl(idx, null);
}
});
this._polyline =
this._polygon =
this._coordSys =
this._points =
this._stackedOnPoints =
this._data = null;
}
});