define(function(require) { 'use strict'; var zrUtil = require('zrender/core/util'); var SymbolDraw = require('../helper/SymbolDraw'); var Symbol = require('../helper/Symbol'); var lineAnimationDiff = require('./lineAnimationDiff'); var graphic = require('../../util/graphic'); var polyHelper = require('./poly'); var ChartView = require('../../view/Chart'); 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 getSmooth(smooth) { return typeof (smooth) === 'number' ? smooth : (smooth ? 0.3 : 0); } function getAxisExtentWithGap(axis) { var extent = axis.getExtent(); if (axis.onBand) { // Remove extra 1px to avoid line miter in clipped edge var halfBandWidth = axis.getBandWidth() / 2 - 1; var dir = extent[1] > extent[0] ? 1 : -1; extent[0] += dir * halfBandWidth; extent[1] -= dir * halfBandWidth; } return extent; } function sign(val) { return val >= 0 ? 1 : -1; } /** * @param {module:echarts/coord/cartesian/Cartesian2D|module:echarts/coord/polar/Polar} coordSys * @param {module:echarts/data/List} data * @param {Array.>} points * @private */ function getStackedOnPoints(coordSys, data) { var baseAxis = coordSys.getBaseAxis(); var valueAxis = coordSys.getOtherAxis(baseAxis); var valueStart = baseAxis.onZero ? 0 : valueAxis.scale.getExtent()[0]; var valueDim = valueAxis.dim; var baseDataOffset = valueDim === 'x' || valueDim === 'radius' ? 1 : 0; return data.mapArray([valueDim], function (val, idx) { var stackedOnSameSign; var stackedOn = data.stackedOn; // Find first stacked value with same sign while (stackedOn && sign(stackedOn.get(valueDim, idx)) === sign(val) ) { stackedOnSameSign = stackedOn; break; } var stackedData = []; stackedData[baseDataOffset] = data.get(baseAxis.dim, idx); stackedData[1 - baseDataOffset] = stackedOnSameSign ? stackedOnSameSign.get(valueDim, idx, true) : valueStart; return coordSys.dataToPoint(stackedData); }, true); } function queryDataIndex(data, payload) { if (payload.dataIndex != null) { return payload.dataIndex; } else if (payload.name != null) { return data.indexOfName(payload.name); } } return ChartView.extend({ type: 'line', init: function () { var lineGroup = new graphic.Group(); var symbolDraw = new SymbolDraw(); this.group.add(symbolDraw.group); this.group.add(lineGroup); 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.normal'); var areaStyleModel = seriesModel.getModel('areaStyle.normal'); var points = data.mapArray(data.getItemLayout, true); 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 = ecModel.get('animation'); var isAreaChart = !areaStyleModel.isEmpty(); var stackedOnPoints = getStackedOnPoints(coordSys, data); var showSymbol = !seriesModel.get('notShowSymbol'); var isSymbolIgnore = showSymbol && !isCoordSysPolar && !seriesModel.get('showAllSymbol') && this._getSymbolIgnoreFunc(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 notShowSymbol changed to true if (!showSymbol) { symbolDraw.remove(); } // Initialization animation or coordinate system changed if ( !(polyline && prevCoordSys.type === coordSys.type) ) { showSymbol && symbolDraw.updateData(data, isSymbolIgnore); polyline = this._newPolyline(group, points, coordSys, hasAnimation); if (isAreaChart) { polygon = this._newPolygon( group, points, stackedOnPoints, coordSys, hasAnimation ); } lineGroup.setClipPath( this._createClipShape(coordSys, true, seriesModel) ); } else { // Update clipPath if (hasAnimation) { lineGroup.setClipPath( this._createClipShape(coordSys, false, seriesModel) ); } // Always update, or it is wrong in the case turning on legend // because points are not changed showSymbol && symbolDraw.updateData(data, isSymbolIgnore); // 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 ); } else { polyline.setShape({ points: points }); polygon && polygon.setShape({ points: points, stackedOnPoints: stackedOnPoints }); } } // Add back group.add(lineGroup); } polyline.setStyle(zrUtil.defaults( // Use color in lineStyle first lineStyleModel.getLineStyle(), { stroke: data.getVisual('color'), lineJoin: 'bevel' } )); var smooth = seriesModel.get('smooth'); smooth = getSmooth(seriesModel.get('smooth')); polyline.shape.smooth = smooth; if (polygon) { var polygonShape = polygon.shape; var stackedOn = data.stackedOn; var stackedOnSmooth = 0; polygon.style.opacity = 0.7; polygon.setStyle(zrUtil.defaults( areaStyleModel.getAreaStyle(), { fill: data.getVisual('color'), lineJoin: 'bevel' } )); polygonShape.smooth = smooth; if (stackedOn) { var stackedOnSeries = stackedOn.hostModel; stackedOnSmooth = getSmooth(stackedOnSeries.get('smooth')); } polygonShape.stackedOnSmooth = stackedOnSmooth; } this._data = data; // Save the coordinate system for transition animation when data changed this._coordSys = coordSys; this._stackedOnPoints = stackedOnPoints; this._points = points; }, highlight: function (seriesModel, ecModel, api, payload) { var data = seriesModel.getData(); var dataIndex = queryDataIndex(data, payload); if (dataIndex != null && dataIndex >= 0) { var symbol = data.getItemGraphicEl(dataIndex); if (!symbol) { // Create a temporary symbol if it is not exists symbol = new Symbol(data, dataIndex, api); symbol.position = data.getItemLayout(dataIndex); symbol.setZ( seriesModel.get('zlevel'), seriesModel.get('z') ); symbol.__temp = true; data.setItemGraphicEl(dataIndex, symbol); // Stop scale animation; symbol.childAt(0).stopAnimation(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 = 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 { // Downplay whole series ChartView.prototype.downplay.call( this, seriesModel, ecModel, api, payload ); } }, /** * @param {module:zrender/container/Group} group * @param {Array.>} points * @private */ _newPolyline: function (group, points) { var polyline = this._polyline; // Remove previous created polyline if (polyline) { group.remove(polyline); } polyline = new polyHelper.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.>} stackedOnPoints * @param {Array.>} points * @private */ _newPolygon: function (group, points, stackedOnPoints) { var polygon = this._polygon; // Remove previous created polygon if (polygon) { group.remove(polygon); } polygon = new polyHelper.Polygon({ shape: { points: points, stackedOnPoints: stackedOnPoints }, silent: true }); this._lineGroup.add(polygon); this._polygon = polygon; return polygon; }, /** * @private */ _getSymbolIgnoreFunc: function (data, coordSys) { var categoryAxis = coordSys.getAxesByScale('ordinal')[0]; // `getLabelInterval` is provided by echarts/component/axis if (categoryAxis && categoryAxis.isLabelIgnored) { return zrUtil.bind(categoryAxis.isLabelIgnored, categoryAxis); } }, /** * @private */ // FIXME Two value axis _updateAnimation: function (data, stackedOnPoints, coordSys, api) { var polyline = this._polyline; var polygon = this._polygon; var seriesModel = data.hostModel; var diff = lineAnimationDiff( this._data, data, this._stackedOnPoints, stackedOnPoints, this._coordSys, coordSys ); polyline.shape.points = diff.current; graphic.updateProps(polyline, { shape: { points: diff.next } }, seriesModel); if (polygon) { polygon.setShape({ points: diff.current, stackedOnPoints: diff.stackedOnCurrent }); graphic.updateProps(polygon, { shape: { points: diff.next, stackedOnPoints: diff.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]); } }); } }, _createClipShape: function (coordSys, hasAnimation, seriesModel) { return coordSys.type === 'polar' ? this._createPolarClipShape(coordSys, hasAnimation, seriesModel) : this._createGridClipShape(coordSys, hasAnimation, seriesModel); }, _createGridClipShape: function (cartesian, hasAnimation, seriesModel) { var xExtent = getAxisExtentWithGap(cartesian.getAxis('x')); var yExtent = getAxisExtentWithGap(cartesian.getAxis('y')); var clipPath = new graphic.Rect({ shape: { x: xExtent[0], y: yExtent[0], width: xExtent[1] - xExtent[0], height: yExtent[1] - yExtent[0] } }); if (hasAnimation) { clipPath.shape[cartesian.getBaseAxis().isHorizontal() ? 'width' : 'height'] = 0; graphic.initProps(clipPath, { shape: { width: xExtent[1] - xExtent[0], height: yExtent[1] - yExtent[0] } }, seriesModel); } return clipPath; }, _createPolarClipShape: function (polar, hasAnimation, seriesModel) { var angleAxis = polar.getAngleAxis(); var radiusAxis = polar.getRadiusAxis(); var radiusExtent = radiusAxis.getExtent(); var angleExtent = angleAxis.getExtent(); var RADIAN = Math.PI / 180; var clipPath = new graphic.Sector({ shape: { cx: polar.cx, cy: polar.cy, r0: radiusExtent[0], r: radiusExtent[1], startAngle: -angleExtent[0] * RADIAN, endAngle: -angleExtent[1] * RADIAN, clockwise: angleAxis.inverse } }); if (hasAnimation) { clipPath.shape.endAngle = -angleExtent[0] * RADIAN; graphic.initProps(clipPath, { shape: { endAngle: -angleExtent[1] * RADIAN } }, seriesModel); } return clipPath; }, remove: function (ecModel) { var group = this.group; group.remove(this._lineGroup); this._symbolDraw.remove(true); } }); });