nsion='value'] Default 'value'. can be 'a', 'b', 'c', 'd', 'e'. * @return {number} */ getValue: function (dimension) { var data = this[hostName][dataName]; return data.get(data.getDimension(dimension || 'value'), this.dataIndex); }, /** * @param {Object|string} key * @param {*} [value] */ setVisual: function (key, value) { this.dataIndex >= 0 && this[hostName][dataName].setItemVisual(this.dataIndex, key, value); }, /** * @param {string} key * @return {boolean} */ getVisual: function (key, ignoreParent) { return this[hostName][dataName].getItemVisual(this.dataIndex, key, ignoreParent); }, /** * @param {Object} layout * @return {boolean} [merge=false] */ setLayout: function (layout, merge$$1) { this.dataIndex >= 0 && this[hostName][dataName].setItemLayout(this.dataIndex, layout, merge$$1); }, /** * @return {Object} */ getLayout: function () { return this[hostName][dataName].getItemLayout(this.dataIndex); }, /** * @return {module:zrender/Element} */ getGraphicEl: function () { return this[hostName][dataName].getItemGraphicEl(this.dataIndex); }, /** * @return {number} */ getRawIndex: function () { return this[hostName][dataName].getRawIndex(this.dataIndex); } }; }; mixin(Node, createGraphDataProxyMixin('hostGraph', 'data')); mixin(Edge, createGraphDataProxyMixin('hostGraph', 'edgeData')); Graph.Node = Node; Graph.Edge = Edge; enableClassCheck(Node); enableClassCheck(Edge); var createGraphFromNodeEdge = function (nodes, edges, seriesModel, directed, beforeLink) { // ??? TODO // support dataset? var graph = new Graph(directed); for (var i = 0; i < nodes.length; i++) { graph.addNode(retrieve( // Id, name, dataIndex nodes[i].id, nodes[i].name, i ), i); } var linkNameList = []; var validEdges = []; var linkCount = 0; for (var i = 0; i < edges.length; i++) { var link = edges[i]; var source = link.source; var target = link.target; // addEdge may fail when source or target not exists if (graph.addEdge(source, target, linkCount)) { validEdges.push(link); linkNameList.push(retrieve(link.id, source + ' > ' + target)); linkCount++; } } var coordSys = seriesModel.get('coordinateSystem'); var nodeData; if (coordSys === 'cartesian2d' || coordSys === 'polar') { nodeData = createListFromArray(nodes, seriesModel); } else { // FIXME var coordSysCtor = CoordinateSystemManager.get(coordSys); // FIXME var dimensionNames = createDimensions(nodes, { coordDimensions: ( (coordSysCtor && coordSysCtor.type !== 'view') ? (coordSysCtor.dimensions || []) : [] ).concat(['value']) }); nodeData = new List(dimensionNames, seriesModel); nodeData.initData(nodes); } var edgeData = new List(['value'], seriesModel); edgeData.initData(validEdges, linkNameList); beforeLink && beforeLink(nodeData, edgeData); linkList({ mainData: nodeData, struct: graph, structAttr: 'graph', datas: {node: nodeData, edge: edgeData}, datasAttr: {node: 'data', edge: 'edgeData'} }); // Update dataIndex of nodes and edges because invalid edge may be removed graph.update(); return graph; }; var GraphSeries = extendSeriesModel({ type: 'series.graph', init: function (option) { GraphSeries.superApply(this, 'init', arguments); // Provide data for legend select this.legendDataProvider = function () { return this._categoriesData; }; this.fillDataTextStyle(option.edges || option.links); this._updateCategoriesData(); }, mergeOption: function (option) { GraphSeries.superApply(this, 'mergeOption', arguments); this.fillDataTextStyle(option.edges || option.links); this._updateCategoriesData(); }, mergeDefaultAndTheme: function (option) { GraphSeries.superApply(this, 'mergeDefaultAndTheme', arguments); defaultEmphasis(option, ['edgeLabel'], ['show']); }, getInitialData: function (option, ecModel) { var edges = option.edges || option.links || []; var nodes = option.data || option.nodes || []; var self = this; if (nodes && edges) { return createGraphFromNodeEdge(nodes, edges, this, true, beforeLink).data; } function beforeLink(nodeData, edgeData) { // Overwrite nodeData.getItemModel to nodeData.wrapMethod('getItemModel', function (model) { var categoriesModels = self._categoriesModels; var categoryIdx = model.getShallow('category'); var categoryModel = categoriesModels[categoryIdx]; if (categoryModel) { categoryModel.parentModel = model.parentModel; model.parentModel = categoryModel; } return model; }); var edgeLabelModel = self.getModel('edgeLabel'); // For option `edgeLabel` can be found by label.xxx.xxx on item mode. var fakeSeriesModel = new Model( {label: edgeLabelModel.option}, edgeLabelModel.parentModel, ecModel ); edgeData.wrapMethod('getItemModel', function (model) { model.customizeGetParent(edgeGetParent); return model; }); function edgeGetParent(path) { path = this.parsePath(path); return (path && path[0] === 'label') ? fakeSeriesModel : this.parentModel; } } }, /** * @return {module:echarts/data/Graph} */ getGraph: function () { return this.getData().graph; }, /** * @return {module:echarts/data/List} */ getEdgeData: function () { return this.getGraph().edgeData; }, /** * @return {module:echarts/data/List} */ getCategoriesData: function () { return this._categoriesData; }, /** * @override */ formatTooltip: function (dataIndex, multipleSeries, dataType) { if (dataType === 'edge') { var nodeData = this.getData(); var params = this.getDataParams(dataIndex, dataType); var edge = nodeData.graph.getEdgeByIndex(dataIndex); var sourceName = nodeData.getName(edge.node1.dataIndex); var targetName = nodeData.getName(edge.node2.dataIndex); var html = []; sourceName != null && html.push(sourceName); targetName != null && html.push(targetName); html = encodeHTML(html.join(' > ')); if (params.value) { html += ' : ' + encodeHTML(params.value); } return html; } else { // dataType === 'node' or empty return GraphSeries.superApply(this, 'formatTooltip', arguments); } }, _updateCategoriesData: function () { var categories = map(this.option.categories || [], function (category) { // Data must has value return category.value != null ? category : extend({ value: 0 }, category); }); var categoriesData = new List(['value'], this); categoriesData.initData(categories); this._categoriesData = categoriesData; this._categoriesModels = categoriesData.mapArray(function (idx) { return categoriesData.getItemModel(idx, true); }); }, setZoom: function (zoom) { this.option.zoom = zoom; }, setCenter: function (center) { this.option.center = center; }, isAnimationEnabled: function () { return GraphSeries.superCall(this, 'isAnimationEnabled') // Not enable animation when do force layout && !(this.get('layout') === 'force' && this.get('force.layoutAnimation')); }, defaultOption: { zlevel: 0, z: 2, coordinateSystem: 'view', // Default option for all coordinate systems // xAxisIndex: 0, // yAxisIndex: 0, // polarIndex: 0, // geoIndex: 0, legendHoverLink: true, hoverAnimation: true, layout: null, focusNodeAdjacency: false, // Configuration of circular layout circular: { rotateLabel: false }, // Configuration of force directed layout force: { initLayout: null, // Node repulsion. Can be an array to represent range. repulsion: [0, 50], gravity: 0.1, // Edge length. Can be an array to represent range. edgeLength: 30, layoutAnimation: true }, left: 'center', top: 'center', // right: null, // bottom: null, // width: '80%', // height: '80%', symbol: 'circle', symbolSize: 10, edgeSymbol: ['none', 'none'], edgeSymbolSize: 10, edgeLabel: { position: 'middle' }, draggable: false, roam: false, // Default on center of graph center: null, zoom: 1, // Symbol size scale ratio in roam nodeScaleRatio: 0.6, // cursor: null, // categories: [], // data: [] // Or // nodes: [] // // links: [] // Or // edges: [] label: { show: false, formatter: '{b}' }, itemStyle: {}, lineStyle: { color: '#aaa', width: 1, curveness: 0, opacity: 0.5 }, emphasis: { label: { show: true } } } }); /** * Line path for bezier and straight line draw */ var straightLineProto = Line.prototype; var bezierCurveProto = BezierCurve.prototype; function isLine(shape) { return isNaN(+shape.cpx1) || isNaN(+shape.cpy1); } var LinePath = extendShape({ type: 'ec-line', style: { stroke: '#000', fill: null }, shape: { x1: 0, y1: 0, x2: 0, y2: 0, percent: 1, cpx1: null, cpy1: null }, buildPath: function (ctx, shape) { (isLine(shape) ? straightLineProto : bezierCurveProto).buildPath(ctx, shape); }, pointAt: function (t) { return isLine(this.shape) ? straightLineProto.pointAt.call(this, t) : bezierCurveProto.pointAt.call(this, t); }, tangentAt: function (t) { var shape = this.shape; var p = isLine(shape) ? [shape.x2 - shape.x1, shape.y2 - shape.y1] : bezierCurveProto.tangentAt.call(this, t); return normalize(p, p); } }); /** * @module echarts/chart/helper/Line */ var SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol']; function makeSymbolTypeKey(symbolCategory) { return '_' + symbolCategory + 'Type'; } /** * @inner */ function createSymbol$1(name, lineData, idx) { var color = lineData.getItemVisual(idx, 'color'); var symbolType = lineData.getItemVisual(idx, name); var symbolSize = lineData.getItemVisual(idx, name + 'Size'); if (!symbolType || symbolType === 'none') { return; } if (!isArray(symbolSize)) { symbolSize = [symbolSize, symbolSize]; } var symbolPath = createSymbol( symbolType, -symbolSize[0] / 2, -symbolSize[1] / 2, symbolSize[0], symbolSize[1], color ); symbolPath.name = name; return symbolPath; } function createLine(points) { var line = new LinePath({ name: 'line' }); setLinePoints(line.shape, points); return line; } function setLinePoints(targetShape, points) { var p1 = points[0]; var p2 = points[1]; var cp1 = points[2]; targetShape.x1 = p1[0]; targetShape.y1 = p1[1]; targetShape.x2 = p2[0]; targetShape.y2 = p2[1]; targetShape.percent = 1; if (cp1) { targetShape.cpx1 = cp1[0]; targetShape.cpy1 = cp1[1]; } else { targetShape.cpx1 = NaN; targetShape.cpy1 = NaN; } } function updateSymbolAndLabelBeforeLineUpdate () { var lineGroup = this; var symbolFrom = lineGroup.childOfName('fromSymbol'); var symbolTo = lineGroup.childOfName('toSymbol'); var label = lineGroup.childOfName('label'); // Quick reject if (!symbolFrom && !symbolTo && label.ignore) { return; } var invScale = 1; var parentNode = this.parent; while (parentNode) { if (parentNode.scale) { invScale /= parentNode.scale[0]; } parentNode = parentNode.parent; } var line = lineGroup.childOfName('line'); // If line not changed // FIXME Parent scale changed if (!this.__dirty && !line.__dirty) { return; } var percent = line.shape.percent; var fromPos = line.pointAt(0); var toPos = line.pointAt(percent); var d = sub([], toPos, fromPos); normalize(d, d); if (symbolFrom) { symbolFrom.attr('position', fromPos); var tangent = line.tangentAt(0); symbolFrom.attr('rotation', Math.PI / 2 - Math.atan2( tangent[1], tangent[0] )); symbolFrom.attr('scale', [invScale * percent, invScale * percent]); } if (symbolTo) { symbolTo.attr('position', toPos); var tangent = line.tangentAt(1); symbolTo.attr('rotation', -Math.PI / 2 - Math.atan2( tangent[1], tangent[0] )); symbolTo.attr('scale', [invScale * percent, invScale * percent]); } if (!label.ignore) { label.attr('position', toPos); var textPosition; var textAlign; var textVerticalAlign; var distance$$1 = 5 * invScale; // End if (label.__position === 'end') { textPosition = [d[0] * distance$$1 + toPos[0], d[1] * distance$$1 + 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'); } // Middle else if (label.__position === 'middle') { var halfPercent = percent / 2; var tangent = line.tangentAt(halfPercent); var n = [tangent[1], -tangent[0]]; var cp = line.pointAt(halfPercent); if (n[1] > 0) { n[0] = -n[0]; n[1] = -n[1]; } textPosition = [cp[0] + n[0] * distance$$1, cp[1] + n[1] * distance$$1]; textAlign = 'center'; textVerticalAlign = 'bottom'; var rotation = -Math.atan2(tangent[1], tangent[0]); if (toPos[0] < fromPos[0]) { rotation = Math.PI + rotation; } label.attr('rotation', rotation); } // Start else { textPosition = [-d[0] * distance$$1 + fromPos[0], -d[1] * distance$$1 + 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'); } label.attr({ style: { // Use the user specified text align and baseline first textVerticalAlign: label.__verticalAlign || textVerticalAlign, textAlign: label.__textAlign || textAlign }, position: textPosition, scale: [invScale, invScale] }); } } /** * @constructor * @extends {module:zrender/graphic/Group} * @alias {module:echarts/chart/helper/Line} */ function Line$1(lineData, idx, seriesScope) { Group.call(this); this._createLine(lineData, idx, seriesScope); } var lineProto = Line$1.prototype; // Update symbol position and rotation lineProto.beforeUpdate = updateSymbolAndLabelBeforeLineUpdate; lineProto._createLine = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var linePoints = lineData.getItemLayout(idx); var line = createLine(linePoints); line.shape.percent = 0; initProps(line, { shape: { percent: 1 } }, seriesModel, idx); this.add(line); var label = new Text({ name: 'label' }); this.add(label); each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbol = createSymbol$1(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); }; lineProto.updateData = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var line = this.childOfName('line'); var linePoints = lineData.getItemLayout(idx); var target = { shape: {} }; setLinePoints(target.shape, linePoints); updateProps(line, target, seriesModel, idx); each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbolType = lineData.getItemVisual(idx, symbolCategory); var key = makeSymbolTypeKey(symbolCategory); // Symbol changed if (this[key] !== symbolType) { this.remove(this.childOfName(symbolCategory)); var symbol = createSymbol$1(symbolCategory, lineData, idx); this.add(symbol); } this[key] = symbolType; }, this); this._updateCommonStl(lineData, idx, seriesScope); }; lineProto._updateCommonStl = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var line = this.childOfName('line'); var lineStyle = seriesScope && seriesScope.lineStyle; var hoverLineStyle = seriesScope && seriesScope.hoverLineStyle; var labelModel = seriesScope && seriesScope.labelModel; var hoverLabelModel = seriesScope && seriesScope.hoverLabelModel; // Optimization for large dataset if (!seriesScope || lineData.hasItemOption) { var itemModel = lineData.getItemModel(idx); lineStyle = itemModel.getModel('lineStyle').getLineStyle(); hoverLineStyle = itemModel.getModel('emphasis.lineStyle').getLineStyle(); labelModel = itemModel.getModel('label'); hoverLabelModel = itemModel.getModel('emphasis.label'); } var visualColor = lineData.getItemVisual(idx, 'color'); var visualOpacity = retrieve3( lineData.getItemVisual(idx, 'opacity'), lineStyle.opacity, 1 ); line.useStyle(defaults( { strokeNoScale: true, fill: 'none', stroke: visualColor, opacity: visualOpacity }, lineStyle )); line.hoverStyle = hoverLineStyle; // Update symbol each$1(SYMBOL_CATEGORIES, function (symbolCategory) { var symbol = this.childOfName(symbolCategory); if (symbol) { symbol.setColor(visualColor); symbol.setStyle({ opacity: visualOpacity }); } }, this); var showLabel = labelModel.getShallow('show'); var hoverShowLabel = hoverLabelModel.getShallow('show'); var label = this.childOfName('label'); var defaultLabelColor; var normalText; var emphasisText; if (showLabel || hoverShowLabel) { defaultLabelColor = visualColor || '#000'; normalText = seriesModel.getFormattedLabel(idx, 'normal', lineData.dataType); if (normalText == null) { var rawVal = seriesModel.getRawValue(idx); normalText = rawVal == null ? lineData.getName(idx) : isFinite(rawVal) ? round$1(rawVal) : rawVal; } emphasisText = retrieve2( seriesModel.getFormattedLabel(idx, 'emphasis', lineData.dataType), normalText ); } // label.afterUpdate = lineAfterUpdate; if (showLabel) { var labelStyle = setTextStyle(label.style, labelModel, { text: normalText }, { autoColor: defaultLabelColor }); label.__textAlign = labelStyle.textAlign; label.__verticalAlign = labelStyle.textVerticalAlign; // 'start', 'middle', 'end' label.__position = labelModel.get('position') || 'middle'; } else { label.setStyle('text', null); } if (hoverShowLabel) { // Only these properties supported in this emphasis style here. label.hoverStyle = { text: emphasisText, textFill: hoverLabelModel.getTextColor(true), // For merging hover style to normal style, do not use // `hoverLabelModel.getFont()` here. fontStyle: hoverLabelModel.getShallow('fontStyle'), fontWeight: hoverLabelModel.getShallow('fontWeight'), fontSize: hoverLabelModel.getShallow('fontSize'), fontFamily: hoverLabelModel.getShallow('fontFamily') }; } else { label.hoverStyle = { text: null }; } label.ignore = !showLabel && !hoverShowLabel; setHoverStyle(this); }; lineProto.highlight = function () { this.trigger('emphasis'); }; lineProto.downplay = function () { this.trigger('normal'); }; lineProto.updateLayout = function (lineData, idx) { this.setLinePoints(lineData.getItemLayout(idx)); }; lineProto.setLinePoints = function (points) { var linePath = this.childOfName('line'); setLinePoints(linePath.shape, points); linePath.dirty(); }; inherits(Line$1, Group); /** * @module echarts/chart/helper/LineDraw */ // import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; /** * @alias module:echarts/component/marker/LineDraw * @constructor */ function LineDraw(ctor) { this._ctor = ctor || Line$1; this.group = new Group(); } var lineDrawProto = LineDraw.prototype; lineDrawProto.isPersistent = function () { return true; }; /** * @param {module:echarts/data/List} lineData */ lineDrawProto.updateData = function (lineData) { var lineDraw = this; var group = lineDraw.group; var oldLineData = lineDraw._lineData; lineDraw._lineData = lineData; // There is no oldLineData only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!oldLineData) { group.removeAll(); } var seriesScope = makeSeriesScope$1(lineData); lineData.diff(oldLineData) .add(function (idx) { doAdd(lineDraw, lineData, idx, seriesScope); }) .update(function (newIdx, oldIdx) { doUpdate(lineDraw, oldLineData, lineData, oldIdx, newIdx, seriesScope); }) .remove(function (idx) { group.remove(oldLineData.getItemGraphicEl(idx)); }) .execute(); }; function doAdd(lineDraw, lineData, idx, seriesScope) { var itemLayout = lineData.getItemLayout(idx); if (!lineNeedsDraw(itemLayout)) { return; } var el = new lineDraw._ctor(lineData, idx, seriesScope); lineData.setItemGraphicEl(idx, el); lineDraw.group.add(el); } function doUpdate(lineDraw, oldLineData, newLineData, oldIdx, newIdx, seriesScope) { var itemEl = oldLineData.getItemGraphicEl(oldIdx); if (!lineNeedsDraw(newLineData.getItemLayout(newIdx))) { lineDraw.group.remove(itemEl); return; } if (!itemEl) { itemEl = new lineDraw._ctor(newLineData, newIdx, seriesScope); } else { itemEl.updateData(newLineData, newIdx, seriesScope); } newLineData.setItemGraphicEl(newIdx, itemEl); lineDraw.group.add(itemEl); } lineDrawProto.updateLayout = function () { var lineData = this._lineData; lineData.eachItemGraphicEl(function (el, idx) { el.updateLayout(lineData, idx); }, this); }; lineDrawProto.incrementalPrepareUpdate = function (lineData) { this._seriesScope = makeSeriesScope$1(lineData); this._lineData = null; this.group.removeAll(); }; lineDrawProto.incrementalUpdate = function (taskParams, lineData) { function updateIncrementalAndHover(el) { if (!el.isGroup) { el.incremental = el.useHoverLayer = true; } } for (var idx = taskParams.start; idx < taskParams.end; idx++) { var itemLayout = lineData.getItemLayout(idx); if (lineNeedsDraw(itemLayout)) { var el = new this._ctor(lineData, idx, this._seriesScope); el.traverse(updateIncrementalAndHover); this.group.add(el); } } }; function makeSeriesScope$1(lineData) { var hostModel = lineData.hostModel; return { lineStyle: hostModel.getModel('lineStyle').getLineStyle(), hoverLineStyle: hostModel.getModel('emphasis.lineStyle').getLineStyle(), labelModel: hostModel.getModel('label'), hoverLabelModel: hostModel.getModel('emphasis.label') }; } lineDrawProto.remove = function () { this._clearIncremental(); this._incremental = null; this.group.removeAll(); }; lineDrawProto._clearIncremental = function () { var incremental = this._incremental; if (incremental) { incremental.clearDisplaybles(); } }; function isPointNaN(pt) { return isNaN(pt[0]) || isNaN(pt[1]); } function lineNeedsDraw(pts) { return !isPointNaN(pts[0]) && !isPointNaN(pts[1]); } var v1 = []; var v2 = []; var v3 = []; var quadraticAt$1 = quadraticAt; var v2DistSquare = distSquare; var mathAbs$1 = Math.abs; function intersectCurveCircle(curvePoints, center, radius) { var p0 = curvePoints[0]; var p1 = curvePoints[1]; var p2 = curvePoints[2]; var d = Infinity; var t; var radiusSquare = radius * radius; var interval = 0.1; for (var _t = 0.1; _t <= 0.9; _t += 0.1) { v1[0] = quadraticAt$1(p0[0], p1[0], p2[0], _t); v1[1] = quadraticAt$1(p0[1], p1[1], p2[1], _t); var diff = mathAbs$1(v2DistSquare(v1, center) - radiusSquare); if (diff < d) { d = diff; t = _t; } } // Assume the segment is monotone,Find root through Bisection method // At most 32 iteration for (var i = 0; i < 32; i++) { // var prev = t - interval; var next = t + interval; // v1[0] = quadraticAt(p0[0], p1[0], p2[0], prev); // v1[1] = quadraticAt(p0[1], p1[1], p2[1], prev); v2[0] = quadraticAt$1(p0[0], p1[0], p2[0], t); v2[1] = quadraticAt$1(p0[1], p1[1], p2[1], t); v3[0] = quadraticAt$1(p0[0], p1[0], p2[0], next); v3[1] = quadraticAt$1(p0[1], p1[1], p2[1], next); var diff = v2DistSquare(v2, center) - radiusSquare; if (mathAbs$1(diff) < 1e-2) { break; } // var prevDiff = v2DistSquare(v1, center) - radiusSquare; var nextDiff = v2DistSquare(v3, center) - radiusSquare; interval /= 2; if (diff < 0) { if (nextDiff >= 0) { t = t + interval; } else { t = t - interval; } } else { if (nextDiff >= 0) { t = t - interval; } else { t = t + interval; } } } return t; } // Adjust edge to avoid var adjustEdge = function (graph, scale$$1) { var tmp0 = []; var quadraticSubdivide$$1 = quadraticSubdivide; var pts = [[], [], []]; var pts2 = [[], []]; var v = []; scale$$1 /= 2; function getSymbolSize(node) { var symbolSize = node.getVisual('symbolSize'); if (symbolSize instanceof Array) { symbolSize = (symbolSize[0] + symbolSize[1]) / 2; } return symbolSize; } graph.eachEdge(function (edge, idx) { var linePoints = edge.getLayout(); var fromSymbol = edge.getVisual('fromSymbol'); var toSymbol = edge.getVisual('toSymbol'); if (!linePoints.__original) { linePoints.__original = [ clone$1(linePoints[0]), clone$1(linePoints[1]) ]; if (linePoints[2]) { linePoints.__original.push(clone$1(linePoints[2])); } } var originalPoints = linePoints.__original; // Quadratic curve if (linePoints[2] != null) { copy(pts[0], originalPoints[0]); copy(pts[1], originalPoints[2]); copy(pts[2], originalPoints[1]); if (fromSymbol && fromSymbol != 'none') { var symbolSize = getSymbolSize(edge.node1); var t = intersectCurveCircle(pts, originalPoints[0], symbolSize * scale$$1); // Subdivide and get the second quadraticSubdivide$$1(pts[0][0], pts[1][0], pts[2][0], t, tmp0); pts[0][0] = tmp0[3]; pts[1][0] = tmp0[4]; quadraticSubdivide$$1(pts[0][1], pts[1][1], pts[2][1], t, tmp0); pts[0][1] = tmp0[3]; pts[1][1] = tmp0[4]; } if (toSymbol && toSymbol != 'none') { var symbolSize = getSymbolSize(edge.node2); var t = intersectCurveCircle(pts, originalPoints[1], symbolSize * scale$$1); // Subdivide and get the first quadraticSubdivide$$1(pts[0][0], pts[1][0], pts[2][0], t, tmp0); pts[1][0] = tmp0[1]; pts[2][0] = tmp0[2]; quadraticSubdivide$$1(pts[0][1], pts[1][1], pts[2][1], t, tmp0); pts[1][1] = tmp0[1]; pts[2][1] = tmp0[2]; } // Copy back to layout copy(linePoints[0], pts[0]); copy(linePoints[1], pts[2]); copy(linePoints[2], pts[1]); } // Line else { copy(pts2[0], originalPoints[0]); copy(pts2[1], originalPoints[1]); sub(v, pts2[1], pts2[0]); normalize(v, v); if (fromSymbol && fromSymbol != 'none') { var symbolSize = getSymbolSize(edge.node1); scaleAndAdd(pts2[0], pts2[0], v, symbolSize * scale$$1); } if (toSymbol && toSymbol != 'none') { var symbolSize = getSymbolSize(edge.node2); scaleAndAdd(pts2[1], pts2[1], v, -symbolSize * scale$$1); } copy(linePoints[0], pts2[0]); copy(linePoints[1], pts2[1]); } }); }; var nodeOpacityPath = ['itemStyle', 'opacity']; var lineOpacityPath = ['lineStyle', 'opacity']; function getItemOpacity(item, opacityPath) { return item.getVisual('opacity') || item.getModel().get(opacityPath); } function fadeOutItem(item, opacityPath, opacityRatio) { var el = item.getGraphicEl(); var opacity = getItemOpacity(item, opacityPath); if (opacityRatio != null) { opacity == null && (opacity = 1); opacity *= opacityRatio; } el.downplay && el.downplay(); el.traverse(function (child) { if (child.type !== 'group') { child.setStyle('opacity', opacity); } }); } function fadeInItem(item, opacityPath) { var opacity = getItemOpacity(item, opacityPath); var el = item.getGraphicEl(); el.highlight && el.highlight(); el.traverse(function (child) { if (child.type !== 'group') { child.setStyle('opacity', opacity); } }); } extendChartView({ type: 'graph', init: function (ecModel, api) { var symbolDraw = new SymbolDraw(); var lineDraw = new LineDraw(); var group = this.group; this._controller = new RoamController(api.getZr()); this._controllerHost = {target: group}; group.add(symbolDraw.group); group.add(lineDraw.group); this._symbolDraw = symbolDraw; this._lineDraw = lineDraw; this._firstRender = true; }, render: function (seriesModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; this._model = seriesModel; this._nodeScaleRatio = seriesModel.get('nodeScaleRatio'); var symbolDraw = this._symbolDraw; var lineDraw = this._lineDraw; var group = this.group; if (coordSys.type === 'view') { var groupNewProp = { position: coordSys.position, scale: coordSys.scale }; if (this._firstRender) { group.attr(groupNewProp); } else { updateProps(group, groupNewProp, seriesModel); } } // Fix edge contact point with node adjustEdge(seriesModel.getGraph(), this._getNodeGlobalScale(seriesModel)); var data = seriesModel.getData(); symbolDraw.updateData(data); var edgeData = seriesModel.getEdgeData(); lineDraw.updateData(edgeData); this._updateNodeAndLinkScale(); this._updateController(seriesModel, ecModel, api); clearTimeout(this._layoutTimeout); var forceLayout = seriesModel.forceLayout; var layoutAnimation = seriesModel.get('force.layoutAnimation'); if (forceLayout) { this._startForceLayoutIteration(forceLayout, layoutAnimation); } data.eachItemGraphicEl(function (el, idx) { var itemModel = data.getItemModel(idx); // Update draggable el.off('drag').off('dragend'); var draggable = data.getItemModel(idx).get('draggable'); if (draggable) { el.on('drag', function () { if (forceLayout) { forceLayout.warmUp(); !this._layouting && this._startForceLayoutIteration(forceLayout, layoutAnimation); forceLayout.setFixed(idx); // Write position back to layout data.setItemLayout(idx, el.position); } }, this).on('dragend', function () { if (forceLayout) { forceLayout.setUnfixed(idx); } }, this); } el.setDraggable(draggable && forceLayout); el.off('mouseover', el.__focusNodeAdjacency); el.off('mouseout', el.__unfocusNodeAdjacency); if (itemModel.get('focusNodeAdjacency')) { el.on('mouseover', el.__focusNodeAdjacency = function () { api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, dataIndex: el.dataIndex }); }); el.on('mouseout', el.__unfocusNodeAdjacency = function () { api.dispatchAction({ type: 'unfocusNodeAdjacency', seriesId: seriesModel.id }); }); } }, this); data.graph.eachEdge(function (edge) { var el = edge.getGraphicEl(); el.off('mouseover', el.__focusNodeAdjacency); el.off('mouseout', el.__unfocusNodeAdjacency); if (edge.getModel().get('focusNodeAdjacency')) { el.on('mouseover', el.__focusNodeAdjacency = function () { api.dispatchAction({ type: 'focusNodeAdjacency', seriesId: seriesModel.id, edgeDataIndex: edge.dataIndex }); }); el.on('mouseout', el.__unfocusNodeAdjacency = function () { api.dispatchAction({ type: 'unfocusNodeAdjacency', seriesId: seriesModel.id }); }); } }); var circularRotateLabel = seriesModel.get('layout') === 'circular' && seriesModel.get('circular.rotateLabel'); var cx = data.getLayout('cx'); var cy = data.getLayout('cy'); data.eachItemGraphicEl(function (el, idx) { var symbolPath = el.getSymbolPath(); if (circularRotateLabel) { var pos = data.getItemLayout(idx); var rad = Math.atan2(pos[1] - cy, pos[0] - cx); if (rad < 0) { rad = Math.PI * 2 + rad; } var isLeft = pos[0] < cx; if (isLeft) { rad = rad - Math.PI; } var textPosition = isLeft ? 'left' : 'right'; symbolPath.setStyle({ textRotation: -rad, textPosition: textPosition, textOrigin: 'center' }); symbolPath.hoverStyle && (symbolPath.hoverStyle.textPosition = textPosition); } else { symbolPath.setStyle({ textRotation: 0 }); } }); this._firstRender = false; }, dispose: function () { this._controller && this._controller.dispose(); this._controllerHost = {}; }, focusNodeAdjacency: function (seriesModel, ecModel, api, payload) { var data = this._model.getData(); var graph = data.graph; var dataIndex = payload.dataIndex; var edgeDataIndex = payload.edgeDataIndex; var node = graph.getNodeByIndex(dataIndex); var edge = graph.getEdgeByIndex(edgeDataIndex); if (!node && !edge) { return; } graph.eachNode(function (node) { fadeOutItem(node, nodeOpacityPath, 0.1); }); graph.eachEdge(function (edge) { fadeOutItem(edge, lineOpacityPath, 0.1); }); if (node) { fadeInItem(node, nodeOpacityPath); each$1(node.edges, function (adjacentEdge) { if (adjacentEdge.dataIndex < 0) { return; } fadeInItem(adjacentEdge, lineOpacityPath); fadeInItem(adjacentEdge.node1, nodeOpacityPath); fadeInItem(adjacentEdge.node2, nodeOpacityPath); }); } if (edge) { fadeInItem(edge, lineOpacityPath); fadeInItem(edge.node1, nodeOpacityPath); fadeInItem(edge.node2, nodeOpacityPath); } }, unfocusNodeAdjacency: function (seriesModel, ecModel, api, payload) { var graph = this._model.getData().graph; graph.eachNode(function (node) { fadeOutItem(node, nodeOpacityPath); }); graph.eachEdge(function (edge) { fadeOutItem(edge, lineOpacityPath); }); }, _startForceLayoutIteration: function (forceLayout, layoutAnimation) { var self = this; (function step() { forceLayout.step(function (stopped) { self.updateLayout(self._model); (self._layouting = !stopped) && ( layoutAnimation ? (self._layoutTimeout = setTimeout(step, 16)) : step() ); }); })(); }, _updateController: function (seriesModel, ecModel, api) { var controller = this._controller; var controllerHost = this._controllerHost; var group = this.group; controller.setPointerChecker(function (e, x, y) { var rect = group.getBoundingRect(); rect.applyTransform(group.transform); return rect.contain(x, y) && !onIrrelevantElement(e, api, seriesModel); }); if (seriesModel.coordinateSystem.type !== 'view') { controller.disable(); return; } controller.enable(seriesModel.get('roam')); controllerHost.zoomLimit = seriesModel.get('scaleLimit'); controllerHost.zoom = seriesModel.coordinateSystem.getZoom(); controller .off('pan') .off('zoom') .on('pan', function (dx, dy) { updateViewOnPan(controllerHost, dx, dy); api.dispatchAction({ seriesId: seriesModel.id, type: 'graphRoam', dx: dx, dy: dy }); }) .on('zoom', function (zoom, mouseX, mouseY) { updateViewOnZoom(controllerHost, zoom, mouseX, mouseY); api.dispatchAction({ seriesId: seriesModel.id, type: 'graphRoam', zoom: zoom, originX: mouseX, originY: mouseY }); this._updateNodeAndLinkScale(); adjustEdge(seriesModel.getGraph(), this._getNodeGlobalScale(seriesModel)); this._lineDraw.updateLayout(); }, this); }, _updateNodeAndLinkScale: function () { var seriesModel = this._model; var data = seriesModel.getData(); var nodeScale = this._getNodeGlobalScale(seriesModel); var invScale = [nodeScale, nodeScale]; data.eachItemGraphicEl(function (el, idx) { el.attr('scale', invScale); }); }, _getNodeGlobalScale: function (seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys.type !== 'view') { return 1; } var nodeScaleRatio = this._nodeScaleRatio; var groupScale = coordSys.scale; var groupZoom = (groupScale && groupScale[0]) || 1; // Scale node when zoom changes var roamZoom = coordSys.getZoom(); var nodeScale = (roamZoom - 1) * nodeScaleRatio + 1; return nodeScale / groupZoom; }, updateLayout: function (seriesModel) { adjustEdge(seriesModel.getGraph(), this._getNodeGlobalScale(seriesModel)); this._symbolDraw.updateLayout(); this._lineDraw.updateLayout(); }, remove: function (ecModel, api) { this._symbolDraw && this._symbolDraw.remove(); this._lineDraw && this._lineDraw.remove(); } }); var actionInfo = { type: 'graphRoam', event: 'graphRoam', update: 'none' }; /** * @payload * @property {string} name Series name * @property {number} [dx] * @property {number} [dy] * @property {number} [zoom] * @property {number} [originX] * @property {number} [originY] */ registerAction(actionInfo, function (payload, ecModel) { ecModel.eachComponent({mainType: 'series', query: payload}, function (seriesModel) { var coordSys = seriesModel.coordinateSystem; var res = updateCenterAndZoom(coordSys, payload); seriesModel.setCenter && seriesModel.setCenter(res.center); seriesModel.setZoom && seriesModel.setZoom(res.zoom); }); }); /** * @payload * @property {number} [seriesIndex] * @property {string} [seriesId] * @property {string} [seriesName] * @property {number} [dataIndex] */ registerAction({ type: 'focusNodeAdjacency', event: 'focusNodeAdjacency', update: 'series.graph:focusNodeAdjacency' }, function () {}); /** * @payload * @property {number} [seriesIndex] * @property {string} [seriesId] * @property {string} [seriesName] */ registerAction({ type: 'unfocusNodeAdjacency', event: 'unfocusNodeAdjacency', update: 'series.graph:unfocusNodeAdjacency' }, function () {}); var categoryFilter = function (ecModel) { var legendModels = ecModel.findComponents({ mainType: 'legend' }); if (!legendModels || !legendModels.length) { return; } ecModel.eachSeriesByType('graph', function (graphSeries) { var categoriesData = graphSeries.getCategoriesData(); var graph = graphSeries.getGraph(); var data = graph.data; var categoryNames = categoriesData.mapArray(categoriesData.getName); data.filterSelf(function (idx) { var model = data.getItemModel(idx); var category = model.getShallow('category'); if (category != null) { if (typeof category === 'number') { category = categoryNames[category]; } // If in any legend component the status is not selected. for (var i = 0; i < legendModels.length; i++) { if (!legendModels[i].isSelected(category)) { return false; } } } return true; }); }, this); }; var categoryVisual = function (ecModel) { var paletteScope = {}; ecModel.eachSeriesByType('graph', function (seriesModel) { var categoriesData = seriesModel.getCategoriesData(); var data = seriesModel.getData(); var categoryNameIdxMap = {}; categoriesData.each(function (idx) { var name = categoriesData.getName(idx); // Add prefix to avoid conflict with Object.prototype. categoryNameIdxMap['ec-' + name] = idx; var itemModel = categoriesData.getItemModel(idx); var color = itemModel.get('itemStyle.color') || seriesModel.getColorFromPalette(name, paletteScope); categoriesData.setItemVisual(idx, 'color', color); }); // Assign category color to visual if (categoriesData.count()) { data.each(function (idx) { var model = data.getItemModel(idx); var category = model.getShallow('category'); if (category != null) { if (typeof category === 'string') { category = categoryNameIdxMap['ec-' + category]; } if (!data.getItemVisual(idx, 'color', true)) { data.setItemVisual( idx, 'color', categoriesData.getItemVisual(category, 'color') ); } } }); } }); }; function normalize$1(a) { if (!(a instanceof Array)) { a = [a, a]; } return a; } var edgeVisual = function (ecModel) { ecModel.eachSeriesByType('graph', function (seriesModel) { var graph = seriesModel.getGraph(); var edgeData = seriesModel.getEdgeData(); var symbolType = normalize$1(seriesModel.get('edgeSymbol')); var symbolSize = normalize$1(seriesModel.get('edgeSymbolSize')); var colorQuery = 'lineStyle.color'.split('.'); var opacityQuery = 'lineStyle.opacity'.split('.'); edgeData.setVisual('fromSymbol', symbolType && symbolType[0]); edgeData.setVisual('toSymbol', symbolType && symbolType[1]); edgeData.setVisual('fromSymbolSize', symbolSize && symbolSize[0]); edgeData.setVisual('toSymbolSize', symbolSize && symbolSize[1]); edgeData.setVisual('color', seriesModel.get(colorQuery)); edgeData.setVisual('opacity', seriesModel.get(opacityQuery)); edgeData.each(function (idx) { var itemModel = edgeData.getItemModel(idx); var edge = graph.getEdgeByIndex(idx); var symbolType = normalize$1(itemModel.getShallow('symbol', true)); var symbolSize = normalize$1(itemModel.getShallow('symbolSize', true)); // Edge visual must after node visual var color = itemModel.get(colorQuery); var opacity = itemModel.get(opacityQuery); switch (color) { case 'source': color = edge.node1.getVisual('color'); break; case 'target': color = edge.node2.getVisual('color'); break; } symbolType[0] && edge.setVisual('fromSymbol', symbolType[0]); symbolType[1] && edge.setVisual('toSymbol', symbolType[1]); symbolSize[0] && edge.setVisual('fromSymbolSize', symbolSize[0]); symbolSize[1] && edge.setVisual('toSymbolSize', symbolSize[1]); edge.setVisual('color', color); edge.setVisual('opacity', opacity); }); }); }; function simpleLayout$1(seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.type !== 'view') { return; } var graph = seriesModel.getGraph(); graph.eachNode(function (node) { var model = node.getModel(); node.setLayout([+model.get('x'), +model.get('y')]); }); simpleLayoutEdge(graph); } function simpleLayoutEdge(graph) { graph.eachEdge(function (edge) { var curveness = edge.getModel().get('lineStyle.curveness') || 0; var p1 = clone$1(edge.node1.getLayout()); var p2 = clone$1(edge.node2.getLayout()); var points = [p1, p2]; if (+curveness) { points.push([ (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * curveness, (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * curveness ]); } edge.setLayout(points); }); } var simpleLayout = function (ecModel, api) { ecModel.eachSeriesByType('graph', function (seriesModel) { var layout = seriesModel.get('layout'); var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.type !== 'view') { var data = seriesModel.getData(); var dimensions = []; each$1(coordSys.dimensions, function (coordDim) { dimensions = dimensions.concat(data.mapDimension(coordDim, true)); }); for (var dataIndex = 0; dataIndex < data.count(); dataIndex++) { var value = []; var hasValue = false; for (var i = 0; i < dimensions.length; i++) { var val = data.get(dimensions[i], dataIndex); if (!isNaN(val)) { hasValue = true; } value.push(val); } if (hasValue) { data.setItemLayout(dataIndex, coordSys.dataToPoint(value)); } else { // Also {Array.<number>}, not undefined to avoid if...else... statement data.setItemLayout(dataIndex, [NaN, NaN]); } } simpleLayoutEdge(data.graph); } else if (!layout || layout === 'none') { simpleLayout$1(seriesModel); } }); }; function circularLayout$1(seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.type !== 'view') { return; } var rect = coordSys.getBoundingRect(); var nodeData = seriesModel.getData(); var graph = nodeData.graph; var angle = 0; var sum = nodeData.getSum('value'); var unitAngle = Math.PI * 2 / (sum || nodeData.count()); var cx = rect.width / 2 + rect.x; var cy = rect.height / 2 + rect.y; var r = Math.min(rect.width, rect.height) / 2; graph.eachNode(function (node) { var value = node.getValue('value'); angle += unitAngle * (sum ? value : 1) / 2; node.setLayout([ r * Math.cos(angle) + cx, r * Math.sin(angle) + cy ]); angle += unitAngle * (sum ? value : 1) / 2; }); nodeData.setLayout({ cx: cx, cy: cy }); graph.eachEdge(function (edge) { var curveness = edge.getModel().get('lineStyle.curveness') || 0; var p1 = clone$1(edge.node1.getLayout()); var p2 = clone$1(edge.node2.getLayout()); var cp1; var x12 = (p1[0] + p2[0]) / 2; var y12 = (p1[1] + p2[1]) / 2; if (+curveness) { curveness *= 3; cp1 = [ cx * curveness + x12 * (1 - curveness), cy * curveness + y12 * (1 - curveness) ]; } edge.setLayout([p1, p2, cp1]); }); } var circularLayout = function (ecModel) { ecModel.eachSeriesByType('graph', function (seriesModel) { if (seriesModel.get('layout') === 'circular') { circularLayout$1(seriesModel); } }); }; var scaleAndAdd$2 = scaleAndAdd; // function adjacentNode(n, e) { // return e.n1 === n ? e.n2 : e.n1; // } function forceLayout$1(nodes, edges, opts) { var rect = opts.rect; var width = rect.width; var height = rect.height; var center = [rect.x + width / 2, rect.y + height / 2]; // var scale = opts.scale || 1; var gravity = opts.gravity == null ? 0.1 : opts.gravity; // for (var i = 0; i < edges.length; i++) { // var e = edges[i]; // var n1 = e.n1; // var n2 = e.n2; // n1.edges = n1.edges || []; // n2.edges = n2.edges || []; // n1.edges.push(e); // n2.edges.push(e); // } // Init position for (var i = 0; i < nodes.length; i++) { var n = nodes[i]; if (!n.p) { // Use the position from first adjecent node with defined position // Or use a random position // From d3 // if (n.edges) { // var j = -1; // while (++j < n.edges.length) { // var e = n.edges[j]; // var other = adjacentNode(n, e); // if (other.p) { // n.p = vec2.clone(other.p); // break; // } // } // } // if (!n.p) { n.p = create( width * (Math.random() - 0.5) + center[0], height * (Math.random() - 0.5) + center[1] ); // } } n.pp = clone$1(n.p); n.edges = null; } // Formula in 'Graph Drawing by Force-directed Placement' // var k = scale * Math.sqrt(width * height / nodes.length); // var k2 = k * k; var friction = 0.6; return { warmUp: function () { friction = 0.5; }, setFixed: function (idx) { nodes[idx].fixed = true; }, setUnfixed: function (idx) { nodes[idx].fixed = false; }, step: function (cb) { var v12 = []; var nLen = nodes.length; for (var i = 0; i < edges.length; i++) { var e = edges[i]; var n1 = e.n1; var n2 = e.n2; sub(v12, n2.p, n1.p); var d = len(v12) - e.d; var w = n2.w / (n1.w + n2.w); if (isNaN(w)) { w = 0; } normalize(v12, v12); !n1.fixed && scaleAndAdd$2(n1.p, n1.p, v12, w * d * friction); !n2.fixed && scaleAndAdd$2(n2.p, n2.p, v12, -(1 - w) * d * friction); } // Gravity for (var i = 0; i < nLen; i++) { var n = nodes[i]; if (!n.fixed) { sub(v12, center, n.p); // var d = vec2.len(v12); // vec2.scale(v12, v12, 1 / d); // var gravityFactor = gravity; scaleAndAdd$2(n.p, n.p, v12, gravity * friction); } } // Repulsive // PENDING for (var i = 0; i < nLen; i++) { var n1 = nodes[i]; for (var j = i + 1; j < nLen; j++) { var n2 = nodes[j]; sub(v12, n2.p, n1.p); var d = len(v12); if (d === 0) { // Random repulse set(v12, Math.random() - 0.5, Math.random() - 0.5); d = 1; } var repFact = (n1.rep + n2.rep) / d / d; !n1.fixed && scaleAndAdd$2(n1.pp, n1.pp, v12, repFact); !n2.fixed && scaleAndAdd$2(n2.pp, n2.pp, v12, -repFact); } } var v = []; for (var i = 0; i < nLen; i++) { var n = nodes[i]; if (!n.fixed) { sub(v, n.p, n.pp); scaleAndAdd$2(n.p, n.p, v, friction); copy(n.pp, n.p); } } friction = friction * 0.992; cb && cb(nodes, edges, friction < 0.01); } }; } var forceLayout = function (ecModel) { ecModel.eachSeriesByType('graph', function (graphSeries) { var coordSys = graphSeries.coordinateSystem; if (coordSys && coordSys.type !== 'view') { return; } if (graphSeries.get('layout') === 'force') { var preservedPoints = graphSeries.preservedPoints || {}; var graph = graphSeries.getGraph(); var nodeData = graph.data; var edgeData = graph.edgeData; var forceModel = graphSeries.getModel('force'); var initLayout = forceModel.get('initLayout'); if (graphSeries.preservedPoints) { nodeData.each(function (idx) { var id = nodeData.getId(idx); nodeData.setItemLayout(idx, preservedPoints[id] || [NaN, NaN]); }); } else if (!initLayout || initLayout === 'none') { simpleLayout$1(graphSeries); } else if (initLayout === 'circular') { circularLayout$1(graphSeries); } var nodeDataExtent = nodeData.getDataExtent('value'); var edgeDataExtent = edgeData.getDataExtent('value'); // var edgeDataExtent = edgeData.getDataExtent('value'); var repulsion = forceModel.get('repulsion'); var edgeLength = forceModel.get('edgeLength'); if (!isArray(repulsion)) { repulsion = [repulsion, repulsion]; } if (!isArray(edgeLength)) { edgeLength = [edgeLength, edgeLength]; } // Larger value has smaller length edgeLength = [edgeLength[1], edgeLength[0]]; var nodes = nodeData.mapArray('value', function (value, idx) { var point = nodeData.getItemLayout(idx); var rep = linearMap(value, nodeDataExtent, repulsion); if (isNaN(rep)) { rep = (repulsion[0] + repulsion[1]) / 2; } return { w: rep, rep: rep, fixed: nodeData.getItemModel(idx).get('fixed'), p: (!point || isNaN(point[0]) || isNaN(point[1])) ? null : point }; }); var edges = edgeData.mapArray('value', function (value, idx) { var edge = graph.getEdgeByIndex(idx); var d = linearMap(value, edgeDataExtent, edgeLength); if (isNaN(d)) { d = (edgeLength[0] + edgeLength[1]) / 2; } return { n1: nodes[edge.node1.dataIndex], n2: nodes[edge.node2.dataIndex], d: d, curveness: edge.getModel().get('lineStyle.curveness') || 0 }; }); var coordSys = graphSeries.coordinateSystem; var rect = coordSys.getBoundingRect(); var forceInstance = forceLayout$1(nodes, edges, { rect: rect, gravity: forceModel.get('gravity') }); var oldStep = forceInstance.step; forceInstance.step = function (cb) { for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i].fixed) { // Write back to layout instance copy(nodes[i].p, graph.getNodeByIndex(i).getLayout()); } } oldStep(function (nodes, edges, stopped) { for (var i = 0, l = nodes.length; i < l; i++) { if (!nodes[i].fixed) { graph.getNodeByIndex(i).setLayout(nodes[i].p); } preservedPoints[nodeData.getId(i)] = nodes[i].p; } for (var i = 0, l = edges.length; i < l; i++) { var e = edges[i]; var edge = graph.getEdgeByIndex(i); var p1 = e.n1.p; var p2 = e.n2.p; var points = edge.getLayout(); points = points ? points.slice() : []; points[0] = points[0] || []; points[1] = points[1] || []; copy(points[0], p1); copy(points[1], p2); if (+e.curveness) { points[2] = [ (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * e.curveness, (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * e.curveness ]; } edge.setLayout(points); } // Update layout cb && cb(stopped); }); }; graphSeries.forceLayout = forceInstance; graphSeries.preservedPoints = preservedPoints; // Step to get the layout forceInstance.step(); } else { // Remove prev injected forceLayout instance graphSeries.forceLayout = null; } }); }; // FIXME Where to create the simple view coordinate system function getViewRect$1(seriesModel, api, aspect) { var option = seriesModel.getBoxLayoutParams(); option.aspect = aspect; return getLayoutRect(option, { width: api.getWidth(), height: api.getHeight() }); } var createView = function (ecModel, api) { var viewList = []; ecModel.eachSeriesByType('graph', function (seriesModel) { var coordSysType = seriesModel.get('coordinateSystem'); if (!coordSysType || coordSysType === 'view') { var data = seriesModel.getData(); var positions = data.mapArray(function (idx) { var itemModel = data.getItemModel(idx); return [+itemModel.get('x'), +itemModel.get('y')]; }); var min = []; var max = []; fromPoints(positions, min, max); // If width or height is 0 if (max[0] - min[0] === 0) { max[0] += 1; min[0] -= 1; } if (max[1] - min[1] === 0) { max[1] += 1; min[1] -= 1; } var aspect = (max[0] - min[0]) / (max[1] - min[1]); // FIXME If get view rect after data processed? var viewRect = getViewRect$1(seriesModel, api, aspect); // Position may be NaN, use view rect instead if (isNaN(aspect)) { min = [viewRect.x, viewRect.y]; max = [viewRect.x + viewRect.width, viewRect.y + viewRect.height]; } var bbWidth = max[0] - min[0]; var bbHeight = max[1] - min[1]; var viewWidth = viewRect.width; var viewHeight = viewRect.height; var viewCoordSys = seriesModel.coordinateSystem = new View(); viewCoordSys.zoomLimit = seriesModel.get('scaleLimit'); viewCoordSys.setBoundingRect( min[0], min[1], bbWidth, bbHeight ); viewCoordSys.setViewRect( viewRect.x, viewRect.y, viewWidth, viewHeight ); // Update roam info viewCoordSys.setCenter(seriesModel.get('center')); viewCoordSys.setZoom(seriesModel.get('zoom')); viewList.push(viewCoordSys); } }); return viewList; }; registerProcessor(categoryFilter); registerVisual(visualSymbol('graph', 'circle', null)); registerVisual(categoryVisual); registerVisual(edgeVisual); registerLayout(simpleLayout); registerLayout(circularLayout); registerLayout(forceLayout); // Graph view coordinate system registerCoordinateSystem('graphView', { create: createView }); var GaugeSeries = SeriesModel.extend({ type: 'series.gauge', getInitialData: function (option, ecModel) { var dataOpt = option.data || []; if (!isArray(dataOpt)) { dataOpt = [dataOpt]; } option.data = dataOpt; return createListSimply(this, ['value']); }, defaultOption: { zlevel: 0, z: 2, // 默认全局居中 center: ['50%', '50%'], legendHoverLink: true, radius: '75%', startAngle: 225, endAngle: -45, clockwise: true, // 最小值 min: 0, // 最大值 max: 100, // 分割段数,默认为10 splitNumber: 10, // 坐标轴线 axisLine: { // 默认显示,属性show控制显示与否 show: true, lineStyle: { // 属性lineStyle控制线条样式 color: [[0.2, '#91c7ae'], [0.8, '#63869e'], [1, '#c23531']], width: 30 } }, // 分隔线 splitLine: { // 默认显示,属性show控制显示与否 show: true, // 属性length控制线长 length: 30, // 属性lineStyle(详见lineStyle)控制线条样式 lineStyle: { color: '#eee', width: 2, type: 'solid' } }, // 坐标轴小标记 axisTick: { // 属性show控制显示与否,默认不显示 show: true, // 每份split细分多少段 splitNumber: 5, // 属性length控制线长 length: 8, // 属性lineStyle控制线条样式 lineStyle: { color: '#eee', width: 1, type: 'solid' } }, axisLabel: { show: true, distance: 5, // formatter: null, color: 'auto' }, pointer: { show: true, length: '80%', width: 8 }, itemStyle: { color: 'auto' }, title: { show: true, // x, y,单位px offsetCenter: [0, '-40%'], // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: '#333', fontSize: 15 }, detail: { show: true, backgroundColor: 'rgba(0,0,0,0)', borderWidth: 0, borderColor: '#ccc', width: 100, height: null, // self-adaption padding: [5, 10], // x, y,单位px offsetCenter: [0, '40%'], // formatter: null, // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: 'auto', fontSize: 30 } } }); var PointerPath = Path.extend({ type: 'echartsGaugePointer', shape: { angle: 0, width: 10, r: 10, x: 0, y: 0 }, buildPath: function (ctx, shape) { var mathCos = Math.cos; var mathSin = Math.sin; var r = shape.r; var width = shape.width; var angle = shape.angle; var x = shape.x - mathCos(angle) * width * (width >= r / 3 ? 1 : 2); var y = shape.y - mathSin(angle) * width * (width >= r / 3 ? 1 : 2); angle = shape.angle - Math.PI / 2; ctx.moveTo(x, y); ctx.lineTo( shape.x + mathCos(angle) * width, shape.y + mathSin(angle) * width ); ctx.lineTo( shape.x + mathCos(shape.angle) * r, shape.y + mathSin(shape.angle) * r ); ctx.lineTo( shape.x - mathCos(angle) * width, shape.y - mathSin(angle) * width ); ctx.lineTo(x, y); return; } }); function parsePosition(seriesModel, api) { var center = seriesModel.get('center'); var width = api.getWidth(); var height = api.getHeight(); var size = Math.min(width, height); var cx = parsePercent$1(center[0], api.getWidth()); var cy = parsePercent$1(center[1], api.getHeight()); var r = parsePercent$1(seriesModel.get('radius'), size / 2); return { cx: cx, cy: cy, r: r }; } function formatLabel(label, labelFormatter) { if (labelFormatter) { if (typeof labelFormatter === 'string') { label = labelFormatter.replace('{value}', label != null ? label : ''); } else if (typeof labelFormatter === 'function') { label = labelFormatter(label); } } return label; } var PI2$5 = Math.PI * 2; var GaugeView = Chart.extend({ type: 'gauge', render: function (seriesModel, ecModel, api) { this.group.removeAll(); var colorList = seriesModel.get('axisLine.lineStyle.color'); var posInfo = parsePosition(seriesModel, api); this._renderMain( seriesModel, ecModel, api, colorList, posInfo ); }, dispose: function () {}, _renderMain: function (seriesModel, ecModel, api, colorList, posInfo) { var group = this.group; var axisLineModel = seriesModel.getModel('axisLine'); var lineStyleModel = axisLineModel.getModel('lineStyle'); var clockwise = seriesModel.get('clockwise'); var startAngle = -seriesModel.get('startAngle') / 180 * Math.PI; var endAngle = -seriesModel.get('endAngle') / 180 * Math.PI; var angleRangeSpan = (endAngle - startAngle) % PI2$5; var prevEndAngle = startAngle; var axisLineWidth = lineStyleModel.get('width'); for (var i = 0; i < colorList.length; i++) { // Clamp var percent = Math.min(Math.max(colorList[i][0], 0), 1); var endAngle = startAngle + angleRangeSpan * percent; var sector = new Sector({ shape: { startAngle: prevEndAngle, endAngle: endAngle, cx: posInfo.cx, cy: posInfo.cy, clockwise: clockwise, r0: posInfo.r - axisLineWidth, r: posInfo.r }, silent: true }); sector.setStyle({ fill: colorList[i][1] }); sector.setStyle(lineStyleModel.getLineStyle( // Because we use sector to simulate arc // so the properties for stroking are useless ['color', 'borderWidth', 'borderColor'] )); group.add(sector); prevEndAngle = endAngle; } var getColor = function (percent) { // Less than 0 if (percent <= 0) { return colorList[0][1]; } for (var i = 0; i < colorList.length; i++) { if (colorList[i][0] >= percent && (i === 0 ? 0 : colorList[i - 1][0]) < percent ) { return colorList[i][1]; } } // More than 1 return colorList[i - 1][1]; }; if (!clockwise) { var tmp = startAngle; startAngle = endAngle; endAngle = tmp; } this._renderTicks( seriesModel, ecModel, api, getColor, posInfo, startAngle, endAngle, clockwise ); this._renderPointer( seriesModel, ecModel, api, getColor, posInfo, startAngle, endAngle, clockwise ); this._renderTitle( seriesModel, ecModel, api, getColor, posInfo ); this._renderDetail( seriesModel, ecModel, api, getColor, posInfo ); }, _renderTicks: function ( seriesModel, ecModel, api, getColor, posInfo, startAngle, endAngle, clockwise ) { var group = this.group; var cx = posInfo.cx; var cy = posInfo.cy; var r = posInfo.r; var minVal = +seriesModel.get('min'); var maxVal = +seriesModel.get('max'); var splitLineModel = seriesModel.getModel('splitLine'); var tickModel = seriesModel.getModel('axisTick'); var labelModel = seriesModel.getModel('axisLabel'); var splitNumber = seriesModel.get('splitNumber'); var subSplitNumber = tickModel.get('splitNumber'); var splitLineLen = parsePercent$1( splitLineModel.get('length'), r ); var tickLen = parsePercent$1( tickModel.get('length'), r ); var angle = startAngle; var step = (endAngle - startAngle) / splitNumber; var subStep = step / subSplitNumber; var splitLineStyle = splitLineModel.getModel('lineStyle').getLineStyle(); var tickLineStyle = tickModel.getModel('lineStyle').getLineStyle(); for (var i = 0; i <= splitNumber; i++) { var unitX = Math.cos(angle); var unitY = Math.sin(angle); // Split line if (splitLineModel.get('show')) { var splitLine = new Line({ shape: { x1: unitX * r + cx, y1: unitY * r + cy, x2: unitX * (r - splitLineLen) + cx, y2: unitY * (r - splitLineLen) + cy }, style: splitLineStyle, silent: true }); if (splitLineStyle.stroke === 'auto') { splitLine.setStyle({ stroke: getColor(i / splitNumber) }); } group.add(splitLine); } // Label if (labelModel.get('show')) { var label = formatLabel( round$1(i / splitNumber * (maxVal - minVal) + minVal), labelModel.get('formatter') ); var distance = labelModel.get('distance'); var autoColor = getColor(i / splitNumber); group.add(new Text({ style: setTextStyle({}, labelModel, { text: label, x: unitX * (r - splitLineLen - distance) + cx, y: unitY * (r - splitLineLen - distance) + cy, textVerticalAlign: unitY < -0.4 ? 'top' : (unitY > 0.4 ? 'bottom' : 'middle'), textAlign: unitX < -0.4 ? 'left' : (unitX > 0.4 ? 'right' : 'center') }, {autoColor: autoColor}), silent: true })); } // Axis tick if (tickModel.get('show') && i !== splitNumber) { for (var j = 0; j <= subSplitNumber; j++) { var unitX = Math.cos(angle); var unitY = Math.sin(angle); var tickLine = new Line({ shape: { x1: unitX * r + cx, y1: unitY * r + cy, x2: unitX * (r - tickLen) + cx, y2: unitY * (r - tickLen) + cy }, silent: true, style: tickLineStyle }); if (tickLineStyle.stroke === 'auto') { tickLine.setStyle({ stroke: getColor((i + j / subSplitNumber) / splitNumber) }); } group.add(tickLine); angle += subStep; } angle -= subStep; } else { angle += step; } } }, _renderPointer: function ( seriesModel, ecModel, api, getColor, posInfo, startAngle, endAngle, clockwise ) { var group = this.group; var oldData = this._data; if (!seriesModel.get('pointer.show')) { // Remove old element oldData && oldData.eachItemGraphicEl(function (el) { group.remove(el); }); return; } var valueExtent = [+seriesModel.get('min'), +seriesModel.get('max')]; var angleExtent = [startAngle, endAngle]; var data = seriesModel.getData(); var valueDim = data.mapDimension('value'); data.diff(oldData) .add(function (idx) { var pointer = new PointerPath({ shape: { angle: startAngle } }); initProps(pointer, { shape: { angle: linearMap(data.get(valueDim, idx), valueExtent, angleExtent, true) } }, seriesModel); group.add(pointer); data.setItemGraphicEl(idx, pointer); }) .update(function (newIdx, oldIdx) { var pointer = oldData.getItemGraphicEl(oldIdx); updateProps(pointer, { shape: { angle: linearMap(data.get(valueDim, newIdx), valueExtent, angleExtent, true) } }, seriesModel); group.add(pointer); data.setItemGraphicEl(newIdx, pointer); }) .remove(function (idx) { var pointer = oldData.getItemGraphicEl(idx); group.remove(pointer); }) .execute(); data.eachItemGraphicEl(function (pointer, idx) { var itemModel = data.getItemModel(idx); var pointerModel = itemModel.getModel('pointer'); pointer.setShape({ x: posInfo.cx, y: posInfo.cy, width: parsePercent$1( pointerModel.get('width'), posInfo.r ), r: parsePercent$1(pointerModel.get('length'), posInfo.r) }); pointer.useStyle(itemModel.getModel('itemStyle').getItemStyle()); if (pointer.style.fill === 'auto') { pointer.setStyle('fill', getColor( linearMap(data.get(valueDim, idx), valueExtent, [0, 1], true) )); } setHoverStyle( pointer, itemModel.getModel('emphasis.itemStyle').getItemStyle() ); }); this._data = data; }, _renderTitle: function ( seriesModel, ecModel, api, getColor, posInfo ) { var data = seriesModel.getData(); var valueDim = data.mapDimension('value'); var titleModel = seriesModel.getModel('title'); if (titleModel.get('show')) { var offsetCenter = titleModel.get('offsetCenter'); var x = posInfo.cx + parsePercent$1(offsetCenter[0], posInfo.r); var y = posInfo.cy + parsePercent$1(offsetCenter[1], posInfo.r); var minVal = +seriesModel.get('min'); var maxVal = +seriesModel.get('max'); var value = seriesModel.getData().get(valueDim, 0); var autoColor = getColor( linearMap(value, [minVal, maxVal], [0, 1], true) ); this.group.add(new Text({ silent: true, style: setTextStyle({}, titleModel, { x: x, y: y, // FIXME First data name ? text: data.getName(0), textAlign: 'center', textVerticalAlign: 'middle' }, {autoColor: autoColor, forceRich: true}) })); } }, _renderDetail: function ( seriesModel, ecModel, api, getColor, posInfo ) { var detailModel = seriesModel.getModel('detail'); var minVal = +seriesModel.get('min'); var maxVal = +seriesModel.get('max'); if (detailModel.get('show')) { var offsetCenter = detailModel.get('offsetCenter'); var x = posInfo.cx + parsePercent$1(offsetCenter[0], posInfo.r); var y = posInfo.cy + parsePercent$1(offsetCenter[1], posInfo.r); var width = parsePercent$1(detailModel.get('width'), posInfo.r); var height = parsePercent$1(detailModel.get('height'), posInfo.r); var data = seriesModel.getData(); var value = data.get(data.mapDimension('value'), 0); var autoColor = getColor( linearMap(value, [minVal, maxVal], [0, 1], true) ); this.group.add(new Text({ silent: true, style: setTextStyle({}, detailModel, { x: x, y: y, text: formatLabel( // FIXME First data name ? value, detailModel.get('formatter') ), textWidth: isNaN(width) ? null : width, textHeight: isNaN(height) ? null : height, textAlign: 'center', textVerticalAlign: 'middle' }, {autoColor: autoColor, forceRich: true}) })); } } }); var FunnelSeries = extendSeriesModel({ type: 'series.funnel', init: function (option) { FunnelSeries.superApply(this, 'init', arguments); // Enable legend selection for each data item // Use a function instead of direct access because data reference may changed this.legendDataProvider = function () { return this.getRawData(); }; // Extend labelLine emphasis this._defaultLabelLine(option); }, getInitialData: function (option, ecModel) { return createListSimply(this, ['value']); }, _defaultLabelLine: function (option) { // Extend labelLine emphasis defaultEmphasis(option, 'labelLine', ['show']); var labelLineNormalOpt = option.labelLine; var labelLineEmphasisOpt = option.emphasis.labelLine; // Not show label line if `label.normal.show = false` labelLineNormalOpt.show = labelLineNormalOpt.show && option.label.show; labelLineEmphasisOpt.show = labelLineEmphasisOpt.show && option.emphasis.label.show; }, // Overwrite getDataParams: function (dataIndex) { var data = this.getData(); var params = FunnelSeries.superCall(this, 'getDataParams', dataIndex); var valueDim = data.mapDimension('value'); var sum = data.getSum(valueDim); // Percent is 0 if sum is 0 params.percent = !sum ? 0 : +(data.get(valueDim, dataIndex) / sum * 100).toFixed(2); params.$vars.push('percent'); return params; }, defaultOption: { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 legendHoverLink: true, left: 80, top: 60, right: 80, bottom: 60, // width: {totalWidth} - left - right, // height: {totalHeight} - top - bottom, // 默认取数据最小最大值 // min: 0, // max: 100, minSize: '0%', maxSize: '100%', sort: 'descending', // 'ascending', 'descending' gap: 0, funnelAlign: 'center', label: { show: true, position: 'outer' // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 }, labelLine: { show: true, length: 20, lineStyle: { // color: 各异, width: 1, type: 'solid' } }, itemStyle: { // color: 各异, borderColor: '#fff', borderWidth: 1 }, emphasis: { label: { show: true } } } }); /** * Piece of pie including Sector, Label, LabelLine * @constructor * @extends {module:zrender/graphic/Group} */ function FunnelPiece(data, idx) { Group.call(this); var polygon = new Polygon(); var labelLine = new Polyline(); var text = new Text(); this.add(polygon); this.add(labelLine); this.add(text); this.updateData(data, idx, true); // Hover to change label and labelLine function onEmphasis() { labelLine.ignore = labelLine.hoverIgnore; text.ignore = text.hoverIgnore; } function onNormal() { labelLine.ignore = labelLine.normalIgnore; text.ignore = text.normalIgnore; } this.on('emphasis', onEmphasis) .on('normal', onNormal) .on('mouseover', onEmphasis) .on('mouseout', onNormal); } var funnelPieceProto = FunnelPiece.prototype; var opacityAccessPath = ['itemStyle', 'opacity']; funnelPieceProto.updateData = function (data, idx, firstCreate) { var polygon = this.childAt(0); var seriesModel = data.hostModel; var itemModel = data.getItemModel(idx); var layout = data.getItemLayout(idx); var opacity = data.getItemModel(idx).get(opacityAccessPath); opacity = opacity == null ? 1 : opacity; // Reset style polygon.useStyle({}); if (firstCreate) { polygon.setShape({ points: layout.points }); polygon.setStyle({ opacity : 0 }); initProps(polygon, { style: { opacity: opacity } }, seriesModel, idx); } else { updateProps(polygon, { style: { opacity: opacity }, shape: { points: layout.points } }, seriesModel, idx); } // Update common style var itemStyleModel = itemModel.getModel('itemStyle'); var visualColor = data.getItemVisual(idx, 'color'); polygon.setStyle( defaults( { lineJoin: 'round', fill: visualColor }, itemStyleModel.getItemStyle(['opacity']) ) ); polygon.hoverStyle = itemStyleModel.getModel('emphasis').getItemStyle(); this._updateLabel(data, idx); setHoverStyle(this); }; funnelPieceProto._updateLabel = function (data, idx) { var labelLine = this.childAt(1); var labelText = this.childAt(2); var seriesModel = data.hostModel; var itemModel = data.getItemModel(idx); var layout = data.getItemLayout(idx); var labelLayout = layout.label; var visualColor = data.getItemVisual(idx, 'color'); updateProps(labelLine, { shape: { points: labelLayout.linePoints || labelLayout.linePoints } }, seriesModel, idx); updateProps(labelText, { style: { x: labelLayout.x, y: labelLayout.y } }, seriesModel, idx); labelText.attr({ rotation: labelLayout.rotation, origin: [labelLayout.x, labelLayout.y], z2: 10 }); var labelModel = itemModel.getModel('label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var labelLineModel = itemModel.getModel('labelLine'); var labelLineHoverModel = itemModel.getModel('emphasis.labelLine'); var visualColor = data.getItemVisual(idx, 'color'); setLabelStyle( labelText.style, labelText.hoverStyle = {}, labelModel, labelHoverModel, { labelFetcher: data.hostModel, labelDataIndex: idx, defaultText: data.getName(idx), autoColor: visualColor, useInsideStyle: !!labelLayout.inside }, { textAlign: labelLayout.textAlign, textVerticalAlign: labelLayout.verticalAlign } ); labelText.ignore = labelText.normalIgnore = !labelModel.get('show'); labelText.hoverIgnore = !labelHoverModel.get('show'); labelLine.ignore = labelLine.normalIgnore = !labelLineModel.get('show'); labelLine.hoverIgnore = !labelLineHoverModel.get('show'); // Default use item visual color labelLine.setStyle({ stroke: visualColor }); labelLine.setStyle(labelLineModel.getModel('lineStyle').getLineStyle()); labelLine.hoverStyle = labelLineHoverModel.getModel('lineStyle').getLineStyle(); }; inherits(FunnelPiece, Group); var FunnelView = Chart.extend({ type: 'funnel', render: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var oldData = this._data; var group = this.group; data.diff(oldData) .add(function (idx) { var funnelPiece = new FunnelPiece(data, idx); data.setItemGraphicEl(idx, funnelPiece); group.add(funnelPiece); }) .update(function (newIdx, oldIdx) { var piePiece = oldData.getItemGraphicEl(oldIdx); piePiece.updateData(data, newIdx); group.add(piePiece); data.setItemGraphicEl(newIdx, piePiece); }) .remove(function (idx) { var piePiece = oldData.getItemGraphicEl(idx); group.remove(piePiece); }) .execute(); this._data = data; }, remove: function () { this.group.removeAll(); this._data = null; }, dispose: function () {} }); function getViewRect$2(seriesModel, api) { return getLayoutRect( seriesModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() } ); } function getSortedIndices(data, sort) { var valueDim = data.mapDimension('value'); var valueArr = data.mapArray(valueDim, function (val) { return val; }); var indices = []; var isAscending = sort === 'ascending'; for (var i = 0, len = data.count(); i < len; i++) { indices[i] = i; } // Add custom sortable function & none sortable opetion by "options.sort" if (typeof sort === 'function') { indices.sort(sort); } else if (sort !== 'none') { indices.sort(function (a, b) { return isAscending ? valueArr[a] - valueArr[b] : valueArr[b] - valueArr[a]; }); } return indices; } function labelLayout$1(data) { data.each(function (idx) { var itemModel = data.getItemModel(idx); var labelModel = itemModel.getModel('label'); var labelPosition = labelModel.get('position'); var labelLineModel = itemModel.getModel('labelLine'); var layout = data.getItemLayout(idx); var points = layout.points; var isLabelInside = labelPosition === 'inner' || labelPosition === 'inside' || labelPosition === 'center'; var textAlign; var textX; var textY; var linePoints; if (isLabelInside) { textX = (points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4; textY = (points[0][1] + points[1][1] + points[2][1] + points[3][1]) / 4; textAlign = 'center'; linePoints = [ [textX, textY], [textX, textY] ]; } else { var x1; var y1; var x2; var labelLineLen = labelLineModel.get('length'); if (labelPosition === 'left') { // Left side x1 = (points[3][0] + points[0][0]) / 2; y1 = (points[3][1] + points[0][1]) / 2; x2 = x1 - labelLineLen; textX = x2 - 5; textAlign = 'right'; } else { // Right side x1 = (points[1][0] + points[2][0]) / 2; y1 = (points[1][1] + points[2][1]) / 2; x2 = x1 + labelLineLen; textX = x2 + 5; textAlign = 'left'; } var y2 = y1; linePoints = [[x1, y1], [x2, y2]]; textY = y2; } layout.label = { linePoints: linePoints, x: textX, y: textY, verticalAlign: 'middle', textAlign: textAlign, inside: isLabelInside }; }); } var funnelLayout = function (ecModel, api, payload) { ecModel.eachSeriesByType('funnel', function (seriesModel) { var data = seriesModel.getData(); var valueDim = data.mapDimension('value'); var sort = seriesModel.get('sort'); var viewRect = getViewRect$2(seriesModel, api); var indices = getSortedIndices(data, sort); var sizeExtent = [ parsePercent$1(seriesModel.get('minSize'), viewRect.width), parsePercent$1(seriesModel.get('maxSize'), viewRect.width) ]; var dataExtent = data.getDataExtent(valueDim); var min = seriesModel.get('min'); var max = seriesModel.get('max'); if (min == null) { min = Math.min(dataExtent[0], 0); } if (max == null) { max = dataExtent[1]; } var funnelAlign = seriesModel.get('funnelAlign'); var gap = seriesModel.get('gap'); var itemHeight = (viewRect.height - gap * (data.count() - 1)) / data.count(); var y = viewRect.y; var getLinePoints = function (idx, offY) { // End point index is data.count() and we assign it 0 var val = data.get(valueDim, idx) || 0; var itemWidth = linearMap(val, [min, max], sizeExtent, true); var x0; switch (funnelAlign) { case 'left': x0 = viewRect.x; break; case 'center': x0 = viewRect.x + (viewRect.width - itemWidth) / 2; break; case 'right': x0 = viewRect.x + viewRect.width - itemWidth; break; } return [ [x0, offY], [x0 + itemWidth, offY] ]; }; if (sort === 'ascending') { // From bottom to top itemHeight = -itemHeight; gap = -gap; y += viewRect.height; indices = indices.reverse(); } for (var i = 0; i < indices.length; i++) { var idx = indices[i]; var nextIdx = indices[i + 1]; var itemModel = data.getItemModel(idx); var height = itemModel.get('itemStyle.height'); if (height == null) { height = itemHeight; } else { height = parsePercent$1(height, viewRect.height); if (sort === 'ascending') { height = -height; } } var start = getLinePoints(idx, y); var end = getLinePoints(nextIdx, y + height); y += height + gap; data.setItemLayout(idx, { points: start.concat(end.slice().reverse()) }); } labelLayout$1(data); }); }; registerVisual(dataColor('funnel')); registerLayout(funnelLayout); registerProcessor(dataFilter('funnel')); var parallelPreprocessor = function (option) { createParallelIfNeeded(option); mergeAxisOptionFromParallel(option); }; /** * Create a parallel coordinate if not exists. * @inner */ function createParallelIfNeeded(option) { if (option.parallel) { return; } var hasParallelSeries = false; each$1(option.series, function (seriesOpt) { if (seriesOpt && seriesOpt.type === 'parallel') { hasParallelSeries = true; } }); if (hasParallelSeries) { option.parallel = [{}]; } } /** * Merge aixs definition from parallel option (if exists) to axis option. * @inner */ function mergeAxisOptionFromParallel(option) { var axes = normalizeToArray(option.parallelAxis); each$1(axes, function (axisOption) { if (!isObject$1(axisOption)) { return; } var parallelIndex = axisOption.parallelIndex || 0; var parallelOption = normalizeToArray(option.parallel)[parallelIndex]; if (parallelOption && parallelOption.parallelAxisDefault) { merge(axisOption, parallelOption.parallelAxisDefault, false); } }); } /** * @constructor module:echarts/coord/parallel/ParallelAxis * @extends {module:echarts/coord/Axis} * @param {string} dim * @param {*} scale * @param {Array.<number>} coordExtent * @param {string} axisType */ var ParallelAxis = function (dim, scale, coordExtent, axisType, axisIndex) { Axis.call(this, dim, scale, coordExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = axisType || 'value'; /** * @type {number} * @readOnly */ this.axisIndex = axisIndex; }; ParallelAxis.prototype = { constructor: ParallelAxis, /** * Axis model * @param {module:echarts/coord/parallel/AxisModel} */ model: null, /** * @override */ isHorizontal: function () { return this.coordinateSystem.getModel().get('layout') !== 'horizontal'; } }; inherits(ParallelAxis, Axis); /** * Calculate slider move result. * Usage: * (1) If both handle0 and handle1 are needed to be moved, set minSpan the same as * maxSpan and the same as `Math.abs(handleEnd[1] - handleEnds[0])`. * (2) If handle0 is forbidden to cross handle1, set minSpan as `0`. * * @param {number} delta Move length. * @param {Array.<number>} handleEnds handleEnds[0] can be bigger then handleEnds[1]. * handleEnds will be modified in this method. * @param {Array.<number>} extent handleEnds is restricted by extent. * extent[0] should less or equals than extent[1]. * @param {number|string} handleIndex Can be 'all', means that both move the two handleEnds, * where the input minSpan and maxSpan will not work. * @param {number} [minSpan] The range of dataZoom can not be smaller than that. * If not set, handle0 and cross handle1. If set as a non-negative * number (including `0`), handles will push each other when reaching * the minSpan. * @param {number} [maxSpan] The range of dataZoom can not be larger than that. * @return {Array.<number>} The input handleEnds. */ var sliderMove = function (delta, handleEnds, extent, handleIndex, minSpan, maxSpan) { // Normalize firstly. handleEnds[0] = restrict$1(handleEnds[0], extent); handleEnds[1] = restrict$1(handleEnds[1], extent); delta = delta || 0; var extentSpan = extent[1] - extent[0]; // Notice maxSpan and minSpan can be null/undefined. if (minSpan != null) { minSpan = restrict$1(minSpan, [0, extentSpan]); } if (maxSpan != null) { maxSpan = Math.max(maxSpan, minSpan != null ? minSpan : 0); } if (handleIndex === 'all') { minSpan = maxSpan = Math.abs(handleEnds[1] - handleEnds[0]); handleIndex = 0; } var originalDistSign = getSpanSign(handleEnds, handleIndex); handleEnds[handleIndex] += delta; // Restrict in extent. var extentMinSpan = minSpan || 0; var realExtent = extent.slice(); originalDistSign.sign < 0 ? (realExtent[0] += extentMinSpan) : (realExtent[1] -= extentMinSpan); handleEnds[handleIndex] = restrict$1(handleEnds[handleIndex], realExtent); // Expand span. var currDistSign = getSpanSign(handleEnds, handleIndex); if (minSpan != null && ( currDistSign.sign !== originalDistSign.sign || currDistSign.span < minSpan )) { // If minSpan exists, 'cross' is forbinden. handleEnds[1 - handleIndex] = handleEnds[handleIndex] + originalDistSign.sign * minSpan; } // Shrink span. var currDistSign = getSpanSign(handleEnds, handleIndex); if (maxSpan != null && currDistSign.span > maxSpan) { handleEnds[1 - handleIndex] = handleEnds[handleIndex] + currDistSign.sign * maxSpan; } return handleEnds; }; function getSpanSign(handleEnds, handleIndex) { var dist = handleEnds[handleIndex] - handleEnds[1 - handleIndex]; // If `handleEnds[0] === handleEnds[1]`, always believe that handleEnd[0] // is at left of handleEnds[1] for non-cross case. return {span: Math.abs(dist), sign: dist > 0 ? -1 : dist < 0 ? 1 : handleIndex ? -1 : 1}; } function restrict$1(value, extend) { return Math.min(extend[1], Math.max(extend[0], value)); } /** * Parallel Coordinates * <https://en.wikipedia.org/wiki/Parallel_coordinates> */ var each$12 = each$1; var mathMin$5 = Math.min; var mathMax$5 = Math.max; var mathFloor$2 = Math.floor; var mathCeil$2 = Math.ceil; var round$2 = round$1; var PI$3 = Math.PI; function Parallel(parallelModel, ecModel, api) { /** * key: dimension * @type {Object.<string, module:echarts/coord/parallel/Axis>} * @private */ this._axesMap = createHashMap(); /** * key: dimension * value: {position: [], rotation, } * @type {Object.<string, Object>} * @private */ this._axesLayout = {}; /** * Always follow axis order. * @type {Array.<string>} * @readOnly */ this.dimensions = parallelModel.dimensions; /** * @type {module:zrender/core/BoundingRect} */ this._rect; /** * @type {module:echarts/coord/parallel/ParallelModel} */ this._model = parallelModel; this._init(parallelModel, ecModel, api); } Parallel.prototype = { type: 'parallel', constructor: Parallel, /** * Initialize cartesian coordinate systems * @private */ _init: function (parallelModel, ecModel, api) { var dimensions = parallelModel.dimensions; var parallelAxisIndex = parallelModel.parallelAxisIndex; each$12(dimensions, function (dim, idx) { var axisIndex = parallelAxisIndex[idx]; var axisModel = ecModel.getComponent('parallelAxis', axisIndex); var axis = this._axesMap.set(dim, new ParallelAxis( dim, createScaleByModel(axisModel), [0, 0], axisModel.get('type'), axisIndex )); var isCategory = axis.type === 'category'; axis.onBand = isCategory && axisModel.get('boundaryGap'); axis.inverse = axisModel.get('inverse'); // Injection axisModel.axis = axis; axis.model = axisModel; axis.coordinateSystem = axisModel.coordinateSystem = this; }, this); }, /** * Update axis scale after data processed * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ update: function (ecModel, api) { this._updateAxesFromSeries(this._model, ecModel); }, /** * @override */ containPoint: function (point) { var layoutInfo = this._makeLayoutInfo(); var axisBase = layoutInfo.axisBase; var layoutBase = layoutInfo.layoutBase; var pixelDimIndex = layoutInfo.pixelDimIndex; var pAxis = point[1 - pixelDimIndex]; var pLayout = point[pixelDimIndex]; return pAxis >= axisBase && pAxis <= axisBase + layoutInfo.axisLength && pLayout >= layoutBase && pLayout <= layoutBase + layoutInfo.layoutLength; }, getModel: function () { return this._model; }, /** * Update properties from series * @private */ _updateAxesFromSeries: function (parallelModel, ecModel) { ecModel.eachSeries(function (seriesModel) { if (!parallelModel.contains(seriesModel, ecModel)) { return; } var data = seriesModel.getData(); each$12(this.dimensions, function (dim) { var axis = this._axesMap.get(dim); axis.scale.unionExtentFromData(data, data.mapDimension(dim)); niceScaleExtent(axis.scale, axis.model); }, this); }, this); }, /** * Resize the parallel coordinate system. * @param {module:echarts/coord/parallel/ParallelModel} parallelModel * @param {module:echarts/ExtensionAPI} api */ resize: function (parallelModel, api) { this._rect = getLayoutRect( parallelModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() } ); this._layoutAxes(); }, /** * @return {module:zrender/core/BoundingRect} */ getRect: function () { return this._rect; }, /** * @private */ _makeLayoutInfo: function () { var parallelModel = this._model; var rect = this._rect; var xy = ['x', 'y']; var wh = ['width', 'height']; var layout = parallelModel.get('layout'); var pixelDimIndex = layout === 'horizontal' ? 0 : 1; var layoutLength = rect[wh[pixelDimIndex]]; var layoutExtent = [0, layoutLength]; var axisCount = this.dimensions.length; var axisExpandWidth = restrict(parallelModel.get('axisExpandWidth'), layoutExtent); var axisExpandCount = restrict(parallelModel.get('axisExpandCount') || 0, [0, axisCount]); var axisExpandable = parallelModel.get('axisExpandable') && axisCount > 3 && axisCount > axisExpandCount && axisExpandCount > 1 && axisExpandWidth > 0 && layoutLength > 0; // `axisExpandWindow` is According to the coordinates of [0, axisExpandLength], // for sake of consider the case that axisCollapseWidth is 0 (when screen is narrow), // where collapsed axes should be overlapped. var axisExpandWindow = parallelModel.get('axisExpandWindow'); var winSize; if (!axisExpandWindow) { winSize = restrict(axisExpandWidth * (axisExpandCount - 1), layoutExtent); var axisExpandCenter = parallelModel.get('axisExpandCenter') || mathFloor$2(axisCount / 2); axisExpandWindow = [axisExpandWidth * axisExpandCenter - winSize / 2]; axisExpandWindow[1] = axisExpandWindow[0] + winSize; } else { winSize = restrict(axisExpandWindow[1] - axisExpandWindow[0], layoutExtent); axisExpandWindow[1] = axisExpandWindow[0] + winSize; } var axisCollapseWidth = (layoutLength - winSize) / (axisCount - axisExpandCount); // Avoid axisCollapseWidth is too small. axisCollapseWidth < 3 && (axisCollapseWidth = 0); // Find the first and last indices > ewin[0] and < ewin[1]. var winInnerIndices = [ mathFloor$2(round$2(axisExpandWindow[0] / axisExpandWidth, 1)) + 1, mathCeil$2(round$2(axisExpandWindow[1] / axisExpandWidth, 1)) - 1 ]; // Pos in ec coordinates. var axisExpandWindow0Pos = axisCollapseWidth / axisExpandWidth * axisExpandWindow[0]; return { layout: layout, pixelDimIndex: pixelDimIndex, layoutBase: rect[xy[pixelDimIndex]], layoutLength: layoutLength, axisBase: rect[xy[1 - pixelDimIndex]], axisLength: rect[wh[1 - pixelDimIndex]], axisExpandable: axisExpandable, axisExpandWidth: axisExpandWidth, axisCollapseWidth: axisCollapseWidth, axisExpandWindow: axisExpandWindow, axisCount: axisCount, winInnerIndices: winInnerIndices, axisExpandWindow0Pos: axisExpandWindow0Pos }; }, /** * @private */ _layoutAxes: function () { var rect = this._rect; var axes = this._axesMap; var dimensions = this.dimensions; var layoutInfo = this._makeLayoutInfo(); var layout = layoutInfo.layout; axes.each(function (axis) { var axisExtent = [0, layoutInfo.axisLength]; var idx = axis.inverse ? 1 : 0; axis.setExtent(axisExtent[idx], axisExtent[1 - idx]); }); each$12(dimensions, function (dim, idx) { var posInfo = (layoutInfo.axisExpandable ? layoutAxisWithExpand : layoutAxisWithoutExpand )(idx, layoutInfo); var positionTable = { horizontal: { x: posInfo.position, y: layoutInfo.axisLength }, vertical: { x: 0, y: posInfo.position } }; var rotationTable = { horizontal: PI$3 / 2, vertical: 0 }; var position = [ positionTable[layout].x + rect.x, positionTable[layout].y + rect.y ]; var rotation = rotationTable[layout]; var transform = create$1(); rotate(transform, transform, rotation); translate(transform, transform, position); // TODO // tick等排布信息。 // TODO // 根据axis order 更新 dimensions顺序。 this._axesLayout[dim] = { position: position, rotation: rotation, transform: transform, axisNameAvailableWidth: posInfo.axisNameAvailableWidth, axisLabelShow: posInfo.axisLabelShow, nameTruncateMaxWidth: posInfo.nameTruncateMaxWidth, tickDirection: 1, labelDirection: 1, labelInterval: axes.get(dim).getLabelInterval() }; }, this); }, /** * Get axis by dim. * @param {string} dim * @return {module:echarts/coord/parallel/ParallelAxis} [description] */ getAxis: function (dim) { return this._axesMap.get(dim); }, /** * Convert a dim value of a single item of series data to Point. * @param {*} value * @param {string} dim * @return {Array} */ dataToPoint: function (value, dim) { return this.axisCoordToPoint( this._axesMap.get(dim).dataToCoord(value), dim ); }, /** * Travel data for one time, get activeState of each data item. * @param {module:echarts/data/List} data * @param {Functio} cb param: {string} activeState 'active' or 'inactive' or 'normal' * {number} dataIndex * @param {Object} context */ eachActiveState: function (data, callback, context) { var dimensions = this.dimensions; var dataDimensions = map(dimensions, function (axisDim) { return data.mapDimension(axisDim); }); var axesMap = this._axesMap; var hasActiveSet = this.hasAxisBrushed(); for (var i = 0, len = data.count(); i < len; i++) { var values = data.getValues(dataDimensions, i); var activeState; if (!hasActiveSet) { activeState = 'normal'; } else { activeState = 'active'; for (var j = 0, lenj = dimensions.length; j < lenj; j++) { var dimName = dimensions[j]; var state = axesMap.get(dimName).model.getActiveState(values[j], j); if (state === 'inactive') { activeState = 'inactive'; break; } } } callback.call(context, activeState, i); } }, /** * Whether has any activeSet. * @return {boolean} */ hasAxisBrushed: function () { var dimensions = this.dimensions; var axesMap = this._axesMap; var hasActiveSet = false; for (var j = 0, lenj = dimensions.length; j < lenj; j++) { if (axesMap.get(dimensions[j]).model.getActiveState() !== 'normal') { hasActiveSet = true; } } return hasActiveSet; }, /** * Convert coords of each axis to Point. * Return point. For example: [10, 20] * @param {Array.<number>} coords * @param {string} dim * @return {Array.<number>} */ axisCoordToPoint: function (coord, dim) { var axisLayout = this._axesLayout[dim]; return applyTransform$1([coord, 0], axisLayout.transform); }, /** * Get axis layout. */ getAxisLayout: function (dim) { return clone(this._axesLayout[dim]); }, /** * @param {Array.<number>} point * @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}. */ getSlidedAxisExpandWindow: function (point) { var layoutInfo = this._makeLayoutInfo(); var pixelDimIndex = layoutInfo.pixelDimIndex; var axisExpandWindow = layoutInfo.axisExpandWindow.slice(); var winSize = axisExpandWindow[1] - axisExpandWindow[0]; var extent = [0, layoutInfo.axisExpandWidth * (layoutInfo.axisCount - 1)]; // Out of the area of coordinate system. if (!this.containPoint(point)) { return {behavior: 'none', axisExpandWindow: axisExpandWindow}; } // Conver the point from global to expand coordinates. var pointCoord = point[pixelDimIndex] - layoutInfo.layoutBase - layoutInfo.axisExpandWindow0Pos; // For dragging operation convenience, the window should not be // slided when mouse is the center area of the window. var delta; var behavior = 'slide'; var axisCollapseWidth = layoutInfo.axisCollapseWidth; var triggerArea = this._model.get('axisExpandSlideTriggerArea'); // But consider touch device, jump is necessary. var useJump = triggerArea[0] != null; if (axisCollapseWidth) { if (useJump && axisCollapseWidth && pointCoord < winSize * triggerArea[0]) { behavior = 'jump'; delta = pointCoord - winSize * triggerArea[2]; } else if (useJump && axisCollapseWidth && pointCoord > winSize * (1 - triggerArea[0])) { behavior = 'jump'; delta = pointCoord - winSize * (1 - triggerArea[2]); } else { (delta = pointCoord - winSize * triggerArea[1]) >= 0 && (delta = pointCoord - winSize * (1 - triggerArea[1])) <= 0 && (delta = 0); } delta *= layoutInfo.axisExpandWidth / axisCollapseWidth; delta ? sliderMove(delta, axisExpandWindow, extent, 'all') // Avoid nonsense triger on mousemove. : (behavior = 'none'); } // When screen is too narrow, make it visible and slidable, although it is hard to interact. else { var winSize = axisExpandWindow[1] - axisExpandWindow[0]; var pos = extent[1] * pointCoord / winSize; axisExpandWindow = [mathMax$5(0, pos - winSize / 2)]; axisExpandWindow[1] = mathMin$5(extent[1], axisExpandWindow[0] + winSize); axisExpandWindow[0] = axisExpandWindow[1] - winSize; } return { axisExpandWindow: axisExpandWindow, behavior: behavior }; } }; function restrict(len, extent) { return mathMin$5(mathMax$5(len, extent[0]), extent[1]); } function layoutAxisWithoutExpand(axisIndex, layoutInfo) { var step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1); return { position: step * axisIndex, axisNameAvailableWidth: step, axisLabelShow: true }; } function layoutAxisWithExpand(axisIndex, layoutInfo) { var layoutLength = layoutInfo.layoutLength; var axisExpandWidth = layoutInfo.axisExpandWidth; var axisCount = layoutInfo.axisCount; var axisCollapseWidth = layoutInfo.axisCollapseWidth; var winInnerIndices = layoutInfo.winInnerIndices; var position; var axisNameAvailableWidth = axisCollapseWidth; var axisLabelShow = false; var nameTruncateMaxWidth; if (axisIndex < winInnerIndices[0]) { position = axisIndex * axisCollapseWidth; nameTruncateMaxWidth = axisCollapseWidth; } else if (axisIndex <= winInnerIndices[1]) { position = layoutInfo.axisExpandWindow0Pos + axisIndex * axisExpandWidth - layoutInfo.axisExpandWindow[0]; axisNameAvailableWidth = axisExpandWidth; axisLabelShow = true; } else { position = layoutLength - (axisCount - 1 - axisIndex) * axisCollapseWidth; nameTruncateMaxWidth = axisCollapseWidth; } return { position: position, axisNameAvailableWidth: axisNameAvailableWidth, axisLabelShow: axisLabelShow, nameTruncateMaxWidth: nameTruncateMaxWidth }; } /** * Parallel coordinate system creater. */ function create$2(ecModel, api) { var coordSysList = []; ecModel.eachComponent('parallel', function (parallelModel, idx) { var coordSys = new Parallel(parallelModel, ecModel, api); coordSys.name = 'parallel_' + idx; coordSys.resize(parallelModel, api); parallelModel.coordinateSystem = coordSys; coordSys.model = parallelModel; coordSysList.push(coordSys); }); // Inject the coordinateSystems into seriesModel ecModel.eachSeries(function (seriesModel) { if (seriesModel.get('coordinateSystem') === 'parallel') { var parallelModel = ecModel.queryComponents({ mainType: 'parallel', index: seriesModel.get('parallelIndex'), id: seriesModel.get('parallelId') })[0]; seriesModel.coordinateSystem = parallelModel.coordinateSystem; } }); return coordSysList; } CoordinateSystemManager.register('parallel', {create: create$2}); var AxisModel$2 = ComponentModel.extend({ type: 'baseParallelAxis', /** * @type {module:echarts/coord/parallel/Axis} */ axis: null, /** * @type {Array.<Array.<number>} * @readOnly */ activeIntervals: [], /** * @return {Object} */ getAreaSelectStyle: function () { return makeStyleMapper( [ ['fill', 'color'], ['lineWidth', 'borderWidth'], ['stroke', 'borderColor'], ['width', 'width'], ['opacity', 'opacity'] ] )(this.getModel('areaSelectStyle')); }, /** * The code of this feature is put on AxisModel but not ParallelAxis, * because axisModel can be alive after echarts updating but instance of * ParallelAxis having been disposed. this._activeInterval should be kept * when action dispatched (i.e. legend click). * * @param {Array.<Array<number>>} intervals interval.length === 0 * means set all active. * @public */ setActiveIntervals: function (intervals) { var activeIntervals = this.activeIntervals = clone(intervals); // Normalize if (activeIntervals) { for (var i = activeIntervals.length - 1; i >= 0; i--) { asc(activeIntervals[i]); } } }, /** * @param {number|string} [value] When attempting to detect 'no activeIntervals set', * value can not be input. * @return {string} 'normal': no activeIntervals set, * 'active', * 'inactive'. * @public */ getActiveState: function (value) { var activeIntervals = this.activeIntervals; if (!activeIntervals.length) { return 'normal'; } if (value == null) { return 'inactive'; } for (var i = 0, len = activeIntervals.length; i < len; i++) { if (activeIntervals[i][0] <= value && value <= activeIntervals[i][1]) { return 'active'; } } return 'inactive'; } }); var defaultOption$1 = { type: 'value', /** * @type {Array.<number>} */ dim: null, // 0, 1, 2, ... // parallelIndex: null, areaSelectStyle: { width: 20, borderWidth: 1, borderColor: 'rgba(160,197,232)', color: 'rgba(160,197,232)', opacity: 0.3 }, realtime: true, // Whether realtime update view when select. z: 10 }; merge(AxisModel$2.prototype, axisModelCommonMixin); function getAxisType$1(axisName, option) { return option.type || (option.data ? 'category' : 'value'); } axisModelCreator('parallel', AxisModel$2, getAxisType$1, defaultOption$1); ComponentModel.extend({ type: 'parallel', dependencies: ['parallelAxis'], /** * @type {module:echarts/coord/parallel/Parallel} */ coordinateSystem: null, /** * Each item like: 'dim0', 'dim1', 'dim2', ... * @type {Array.<string>} * @readOnly */ dimensions: null, /** * Coresponding to dimensions. * @type {Array.<number>} * @readOnly */ parallelAxisIndex: null, layoutMode: 'box', defaultOption: { zlevel: 0, z: 0, left: 80, top: 60, right: 80, bottom: 60, // width: {totalWidth} - left - right, // height: {totalHeight} - top - bottom, layout: 'horizontal', // 'horizontal' or 'vertical' // FIXME // naming? axisExpandable: false, axisExpandCenter: null, axisExpandCount: 0, axisExpandWidth: 50, // FIXME '10%' ? axisExpandRate: 17, axisExpandDebounce: 50, // [out, in, jumpTarget]. In percentage. If use [null, 0.05], null means full. // Do not doc to user until necessary. axisExpandSlideTriggerArea: [-0.15, 0.05, 0.4], axisExpandTriggerOn: 'click', // 'mousemove' or 'click' parallelAxisDefault: null }, /** * @override */ init: function () { ComponentModel.prototype.init.apply(this, arguments); this.mergeOption({}); }, /** * @override */ mergeOption: function (newOption) { var thisOption = this.option; newOption && merge(thisOption, newOption, true); this._initDimensions(); }, /** * Whether series or axis is in this coordinate system. * @param {module:echarts/model/Series|module:echarts/coord/parallel/AxisModel} model * @param {module:echarts/model/Global} ecModel */ contains: function (model, ecModel) { var parallelIndex = model.get('parallelIndex'); return parallelIndex != null && ecModel.getComponent('parallel', parallelIndex) === this; }, setAxisExpand: function (opt) { each$1( ['axisExpandable', 'axisExpandCenter', 'axisExpandCount', 'axisExpandWidth', 'axisExpandWindow'], function (name) { if (opt.hasOwnProperty(name)) { this.option[name] = opt[name]; } }, this ); }, /** * @private */ _initDimensions: function () { var dimensions = this.dimensions = []; var parallelAxisIndex = this.parallelAxisIndex = []; var axisModels = filter(this.dependentModels.parallelAxis, function (axisModel) { // Can not use this.contains here, because // initialization has not been completed yet. return (axisModel.get('parallelIndex') || 0) === this.componentIndex; }, this); each$1(axisModels, function (axisModel) { dimensions.push('dim' + axisModel.get('dim')); parallelAxisIndex.push(axisModel.componentIndex); }); } }); /** * @payload * @property {string} parallelAxisId * @property {Array.<Array.<number>>} intervals */ var actionInfo$1 = { type: 'axisAreaSelect', event: 'axisAreaSelected' // update: 'updateVisual' }; registerAction(actionInfo$1, function (payload, ecModel) { ecModel.eachComponent( {mainType: 'parallelAxis', query: payload}, function (parallelAxisModel) { parallelAxisModel.axis.model.setActiveIntervals(payload.intervals); } ); }); /** * @payload */ registerAction('parallelAxisExpand', function (payload, ecModel) { ecModel.eachComponent( {mainType: 'parallel', query: payload}, function (parallelModel) { parallelModel.setAxisExpand(payload); } ); }); var curry$2 = curry; var each$13 = each$1; var map$2 = map; var mathMin$6 = Math.min; var mathMax$6 = Math.max; var mathPow$2 = Math.pow; var COVER_Z = 10000; var UNSELECT_THRESHOLD = 6; var MIN_RESIZE_LINE_WIDTH = 6; var MUTEX_RESOURCE_KEY = 'globalPan'; var DIRECTION_MAP = { w: [0, 0], e: [0, 1], n: [1, 0], s: [1, 1] }; var CURSOR_MAP = { w: 'ew', e: 'ew', n: 'ns', s: 'ns', ne: 'nesw', sw: 'nesw', nw: 'nwse', se: 'nwse' }; var DEFAULT_BRUSH_OPT = { brushStyle: { lineWidth: 2, stroke: 'rgba(0,0,0,0.3)', fill: 'rgba(0,0,0,0.1)' }, transformable: true, brushMode: 'single', removeOnClick: false }; var baseUID = 0; /** * @alias module:echarts/component/helper/BrushController * @constructor * @mixin {module:zrender/mixin/Eventful} * @event module:echarts/component/helper/BrushController#brush * params: * areas: Array.<Array>, coord relates to container group, * If no container specified, to global. * opt { * isEnd: boolean, * removeOnClick: boolean * } * * @param {module:zrender/zrender~ZRender} zr */ function BrushController(zr) { if (__DEV__) { assert$1(zr); } Eventful.call(this); /** * @type {module:zrender/zrender~ZRender} * @private */ this._zr = zr; /** * @type {module:zrender/container/Group} * @readOnly */ this.group = new Group(); /** * Only for drawing (after enabledBrush). * 'line', 'rect', 'polygon' or false * If passing false/null/undefined, disable brush. * If passing 'auto', determined by panel.defaultBrushType * @private * @type {string} */ this._brushType; /** * Only for drawing (after enabledBrush). * * @private * @type {Object} */ this._brushOption; /** * @private * @type {Object} */ this._panels; /** * @private * @type {Array.<nubmer>} */ this._track = []; /** * @private * @type {boolean} */ this._dragging; /** * @private * @type {Array} */ this._covers = []; /** * @private * @type {moudule:zrender/container/Group} */ this._creatingCover; /** * `true` means global panel * @private * @type {module:zrender/container/Group|boolean} */ this._creatingPanel; /** * @private * @type {boolean} */ this._enableGlobalPan; /** * @private * @type {boolean} */ if (__DEV__) { this._mounted; } /** * @private * @type {string} */ this._uid = 'brushController_' + baseUID++; /** * @private * @type {Object} */ this._handlers = {}; each$13(mouseHandlers, function (handler, eventName) { this._handlers[eventName] = bind(handler, this); }, this); } BrushController.prototype = { constructor: BrushController, /** * If set to null/undefined/false, select disabled. * @param {Object} brushOption * @param {string|boolean} brushOption.brushType 'line', 'rect', 'polygon' or false * If passing false/null/undefined, disable brush. * If passing 'auto', determined by panel.defaultBrushType. * ('auto' can not be used in global panel) * @param {number} [brushOption.brushMode='single'] 'single' or 'multiple' * @param {boolean} [brushOption.transformable=true] * @param {boolean} [brushOption.removeOnClick=false] * @param {Object} [brushOption.brushStyle] * @param {number} [brushOption.brushStyle.width] * @param {number} [brushOption.brushStyle.lineWidth] * @param {string} [brushOption.brushStyle.stroke] * @param {string} [brushOption.brushStyle.fill] * @param {number} [brushOption.z] */ enableBrush: function (brushOption) { if (__DEV__) { assert$1(this._mounted); } this._brushType && doDisableBrush(this); brushOption.brushType && doEnableBrush(this, brushOption); return this; }, /** * @param {Array.<Object>} panelOpts If not pass, it is global brush. * Each items: { * panelId, // mandatory. * clipPath, // mandatory. function. * isTargetByCursor, // mandatory. function. * defaultBrushType, // optional, only used when brushType is 'auto'. * getLinearBrushOtherExtent, // optional. function. * } */ setPanels: function (panelOpts) { if (panelOpts && panelOpts.length) { var panels = this._panels = {}; each$1(panelOpts, function (panelOpts) { panels[panelOpts.panelId] = clone(panelOpts); }); } else { this._panels = null; } return this; }, /** * @param {Object} [opt] * @return {boolean} [opt.enableGlobalPan=false] */ mount: function (opt) { opt = opt || {}; if (__DEV__) { this._mounted = true; // should be at first. } this._enableGlobalPan = opt.enableGlobalPan; var thisGroup = this.group; this._zr.add(thisGroup); thisGroup.attr({ position: opt.position || [0, 0], rotation: opt.rotation || 0, scale: opt.scale || [1, 1] }); this._transform = thisGroup.getLocalTransform(); return this; }, eachCover: function (cb, context) { each$13(this._covers, cb, context); }, /** * Update covers. * @param {Array.<Object>} brushOptionList Like: * [ * {id: 'xx', brushType: 'line', range: [23, 44], brushStyle, transformable}, * {id: 'yy', brushType: 'rect', range: [[23, 44], [23, 54]]}, * ... * ] * `brushType` is required in each cover info. (can not be 'auto') * `id` is not mandatory. * `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default. * If brushOptionList is null/undefined, all covers removed. */ updateCovers: function (brushOptionList) { if (__DEV__) { assert$1(this._mounted); } brushOptionList = map(brushOptionList, function (brushOption) { return merge(clone(DEFAULT_BRUSH_OPT), brushOption, true); }); var tmpIdPrefix = '\0-brush-index-'; var oldCovers = this._covers; var newCovers = this._covers = []; var controller = this; var creatingCover = this._creatingCover; (new DataDiffer(oldCovers, brushOptionList, oldGetKey, getKey)) .add(addOrUpdate) .update(addOrUpdate) .remove(remove) .execute(); return this; function getKey(brushOption, index) { return (brushOption.id != null ? brushOption.id : tmpIdPrefix + index) + '-' + brushOption.brushType; } function oldGetKey(cover, index) { return getKey(cover.__brushOption, index); } function addOrUpdate(newIndex, oldIndex) { var newBrushOption = brushOptionList[newIndex]; // Consider setOption in event listener of brushSelect, // where updating cover when creating should be forbiden. if (oldIndex != null && oldCovers[oldIndex] === creatingCover) { newCovers[newIndex] = oldCovers[oldIndex]; } else { var cover = newCovers[newIndex] = oldIndex != null ? ( oldCovers[oldIndex].__brushOption = newBrushOption, oldCovers[oldIndex] ) : endCreating(controller, createCover(controller, newBrushOption)); updateCoverAfterCreation(controller, cover); } } function remove(oldIndex) { if (oldCovers[oldIndex] !== creatingCover) { controller.group.remove(oldCovers[oldIndex]); } } }, unmount: function () { if (__DEV__) { if (!this._mounted) { return; } } this.enableBrush(false); // container may 'removeAll' outside. clearCovers(this); this._zr.remove(this.group); if (__DEV__) { this._mounted = false; // should be at last. } return this; }, dispose: function () { this.unmount(); this.off(); } }; mixin(BrushController, Eventful); function doEnableBrush(controller, brushOption) { var zr = controller._zr; // Consider roam, which takes globalPan too. if (!controller._enableGlobalPan) { take(zr, MUTEX_RESOURCE_KEY, controller._uid); } each$13(controller._handlers, function (handler, eventName) { zr.on(eventName, handler); }); controller._brushType = brushOption.brushType; controller._brushOption = merge(clone(DEFAULT_BRUSH_OPT), brushOption, true); } function doDisableBrush(controller) { var zr = controller._zr; release(zr, MUTEX_RESOURCE_KEY, controller._uid); each$13(controller._handlers, function (handler, eventName) { zr.off(eventName, handler); }); controller._brushType = controller._brushOption = null; } function createCover(controller, brushOption) { var cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption); cover.__brushOption = brushOption; updateZ$1(cover, brushOption); controller.group.add(cover); return cover; } function endCreating(controller, creatingCover) { var coverRenderer = getCoverRenderer(creatingCover); if (coverRenderer.endCreating) { coverRenderer.endCreating(controller, creatingCover); updateZ$1(creatingCover, creatingCover.__brushOption); } return creatingCover; } function updateCoverShape(controller, cover) { var brushOption = cover.__brushOption; getCoverRenderer(cover).updateCoverShape( controller, cover, brushOption.range, brushOption ); } function updateZ$1(cover, brushOption) { var z = brushOption.z; z == null && (z = COVER_Z); cover.traverse(function (el) { el.z = z; el.z2 = z; // Consider in given container. }); } function updateCoverAfterCreation(controller, cover) { getCoverRenderer(cover).updateCommon(controller, cover); updateCoverShape(controller, cover); } function getCoverRenderer(cover) { return coverRenderers[cover.__brushOption.brushType]; } // return target panel or `true` (means global panel) function getPanelByPoint(controller, e, localCursorPoint) { var panels = controller._panels; if (!panels) { return true; // Global panel } var panel; var transform = controller._transform; each$13(panels, function (pn) { pn.isTargetByCursor(e, localCursorPoint, transform) && (panel = pn); }); return panel; } // Return a panel or true function getPanelByCover(controller, cover) { var panels = controller._panels; if (!panels) { return true; // Global panel } var panelId = cover.__brushOption.panelId; // User may give cover without coord sys info, // which is then treated as global panel. return panelId != null ? panels[panelId] : true; } function clearCovers(controller) { var covers = controller._covers; var originalLength = covers.length; each$13(covers, function (cover) { controller.group.remove(cover); }, controller); covers.length = 0; return !!originalLength; } function trigger(controller, opt) { var areas = map$2(controller._covers, function (cover) { var brushOption = cover.__brushOption; var range = clone(brushOption.range); return { brushType: brushOption.brushType, panelId: brushOption.panelId, range: range }; }); controller.trigger('brush', areas, { isEnd: !!opt.isEnd, removeOnClick: !!opt.removeOnClick }); } function shouldShowCover(controller) { var track = controller._track; if (!track.length) { return false; } var p2 = track[track.length - 1]; var p1 = track[0]; var dx = p2[0] - p1[0]; var dy = p2[1] - p1[1]; var dist = mathPow$2(dx * dx + dy * dy, 0.5); return dist > UNSELECT_THRESHOLD; } function getTrackEnds(track) { var tail = track.length - 1; tail < 0 && (tail = 0); return [track[0], track[tail]]; } function createBaseRectCover(doDrift, controller, brushOption, edgeNames) { var cover = new Group(); cover.add(new Rect({ name: 'main', style: makeStyle(brushOption), silent: true, draggable: true, cursor: 'move', drift: curry$2(doDrift, controller, cover, 'nswe'), ondragend: curry$2(trigger, controller, {isEnd: true}) })); each$13( edgeNames, function (name) { cover.add(new Rect({ name: name, style: {opacity: 0}, draggable: true, silent: true, invisible: true, drift: curry$2(doDrift, controller, cover, name), ondragend: curry$2(trigger, controller, {isEnd: true}) })); } ); return cover; } function updateBaseRect(controller, cover, localRange, brushOption) { var lineWidth = brushOption.brushStyle.lineWidth || 0; var handleSize = mathMax$6(lineWidth, MIN_RESIZE_LINE_WIDTH); var x = localRange[0][0]; var y = localRange[1][0]; var xa = x - lineWidth / 2; var ya = y - lineWidth / 2; var x2 = localRange[0][1]; var y2 = localRange[1][1]; var x2a = x2 - handleSize + lineWidth / 2; var y2a = y2 - handleSize + lineWidth / 2; var width = x2 - x; var height = y2 - y; var widtha = width + lineWidth; var heighta = height + lineWidth; updateRectShape(controller, cover, 'main', x, y, width, height); if (brushOption.transformable) { updateRectShape(controller, cover, 'w', xa, ya, handleSize, heighta); updateRectShape(controller, cover, 'e', x2a, ya, handleSize, heighta); updateRectShape(controller, cover, 'n', xa, ya, widtha, handleSize); updateRectShape(controller, cover, 's', xa, y2a, widtha, handleSize); updateRectShape(controller, cover, 'nw', xa, ya, handleSize, handleSize); updateRectShape(controller, cover, 'ne', x2a, ya, handleSize, handleSize); updateRectShape(controller, cover, 'sw', xa, y2a, handleSize, handleSize); updateRectShape(controller, cover, 'se', x2a, y2a, handleSize, handleSize); } } function updateCommon(controller, cover) { var brushOption = cover.__brushOption; var transformable = brushOption.transformable; var mainEl = cover.childAt(0); mainEl.useStyle(makeStyle(brushOption)); mainEl.attr({ silent: !transformable, cursor: transformable ? 'move' : 'default' }); each$13( ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw'], function (name) { var el = cover.childOfName(name); var globalDir = getGlobalDirection(controller, name); el && el.attr({ silent: !transformable, invisible: !transformable, cursor: transformable ? CURSOR_MAP[globalDir] + '-resize' : null }); } ); } function updateRectShape(controller, cover, name, x, y, w, h) { var el = cover.childOfName(name); el && el.setShape(pointsToRect( clipByPanel(controller, cover, [[x, y], [x + w, y + h]]) )); } function makeStyle(brushOption) { return defaults({strokeNoScale: true}, brushOption.brushStyle); } function formatRectRange(x, y, x2, y2) { var min = [mathMin$6(x, x2), mathMin$6(y, y2)]; var max = [mathMax$6(x, x2), mathMax$6(y, y2)]; return [ [min[0], max[0]], // x range [min[1], max[1]] // y range ]; } function getTransform$1(controller) { return getTransform(controller.group); } function getGlobalDirection(controller, localDirection) { if (localDirection.length > 1) { localDirection = localDirection.split(''); var globalDir = [ getGlobalDirection(controller, localDirection[0]), getGlobalDirection(controller, localDirection[1]) ]; (globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse(); return globalDir.join(''); } else { var map$$1 = {w: 'left', e: 'right', n: 'top', s: 'bottom'}; var inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'}; var globalDir = transformDirection( map$$1[localDirection], getTransform$1(controller) ); return inverseMap[globalDir]; } } function driftRect(toRectRange, fromRectRange, controller, cover, name, dx, dy, e) { var brushOption = cover.__brushOption; var rectRange = toRectRange(brushOption.range); var localDelta = toLocalDelta(controller, dx, dy); each$13(name.split(''), function (namePart) { var ind = DIRECTION_MAP[namePart]; rectRange[ind[0]][ind[1]] += localDelta[ind[0]]; }); brushOption.range = fromRectRange(formatRectRange( rectRange[0][0], rectRange[1][0], rectRange[0][1], rectRange[1][1] )); updateCoverAfterCreation(controller, cover); trigger(controller, {isEnd: false}); } function driftPolygon(controller, cover, dx, dy, e) { var range = cover.__brushOption.range; var localDelta = toLocalDelta(controller, dx, dy); each$13(range, function (point) { point[0] += localDelta[0]; point[1] += localDelta[1]; }); updateCoverAfterCreation(controller, cover); trigger(controller, {isEnd: false}); } function toLocalDelta(controller, dx, dy) { var thisGroup = controller.group; var localD = thisGroup.transformCoordToLocal(dx, dy); var localZero = thisGroup.transformCoordToLocal(0, 0); return [localD[0] - localZero[0], localD[1] - localZero[1]]; } function clipByPanel(controller, cover, data) { var panel = getPanelByCover(controller, cover); return (panel && panel !== true) ? panel.clipPath(data, controller._transform) : clone(data); } function pointsToRect(points) { var xmin = mathMin$6(points[0][0], points[1][0]); var ymin = mathMin$6(points[0][1], points[1][1]); var xmax = mathMax$6(points[0][0], points[1][0]); var ymax = mathMax$6(points[0][1], points[1][1]); return { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; } function resetCursor(controller, e, localCursorPoint) { // Check active if (!controller._brushType) { return; } var zr = controller._zr; var covers = controller._covers; var currPanel = getPanelByPoint(controller, e, localCursorPoint); // Check whether in covers. if (!controller._dragging) { for (var i = 0; i < covers.length; i++) { var brushOption = covers[i].__brushOption; if (currPanel && (currPanel === true || brushOption.panelId === currPanel.panelId) && coverRenderers[brushOption.brushType].contain( covers[i], localCursorPoint[0], localCursorPoint[1] ) ) { // Use cursor style set on cover. return; } } } currPanel && zr.setCursorStyle('crosshair'); } function preventDefault(e) { var rawE = e.event; rawE.preventDefault && rawE.preventDefault(); } function mainShapeContain(cover, x, y) { return cover.childOfName('main').contain(x, y); } function updateCoverByMouse(controller, e, localCursorPoint, isEnd) { var creatingCover = controller._creatingCover; var panel = controller._creatingPanel; var thisBrushOption = controller._brushOption; var eventParams; controller._track.push(localCursorPoint.slice()); if (shouldShowCover(controller) || creatingCover) { if (panel && !creatingCover) { thisBrushOption.brushMode === 'single' && clearCovers(controller); var brushOption = clone(thisBrushOption); brushOption.brushType = determineBrushType(brushOption.brushType, panel); brushOption.panelId = panel === true ? null : panel.panelId; creatingCover = controller._creatingCover = createCover(controller, brushOption); controller._covers.push(creatingCover); } if (creatingCover) { var coverRenderer = coverRenderers[determineBrushType(controller._brushType, panel)]; var coverBrushOption = creatingCover.__brushOption; coverBrushOption.range = coverRenderer.getCreatingRange( clipByPanel(controller, creatingCover, controller._track) ); if (isEnd) { endCreating(controller, creatingCover); coverRenderer.updateCommon(controller, creatingCover); } updateCoverShape(controller, creatingCover); eventParams = {isEnd: isEnd}; } } else if ( isEnd && thisBrushOption.brushMode === 'single' && thisBrushOption.removeOnClick ) { // Help user to remove covers easily, only by a tiny drag, in 'single' mode. // But a single click do not clear covers, because user may have casual // clicks (for example, click on other component and do not expect covers // disappear). // Only some cover removed, trigger action, but not every click trigger action. if (getPanelByPoint(controller, e, localCursorPoint) && clearCovers(controller)) { eventParams = {isEnd: isEnd, removeOnClick: true}; } } return eventParams; } function determineBrushType(brushType, panel) { if (brushType === 'auto') { if (__DEV__) { assert$1( panel && panel.defaultBrushType, 'MUST have defaultBrushType when brushType is "atuo"' ); } return panel.defaultBrushType; } return brushType; } var mouseHandlers = { mousedown: function (e) { if (this._dragging) { // In case some browser do not support globalOut, // and release mose out side the browser. handleDragEnd.call(this, e); } else if (!e.target || !e.target.draggable) { preventDefault(e); var localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY); this._creatingCover = null; var panel = this._creatingPanel = getPanelByPoint(this, e, localCursorPoint); if (panel) { this._dragging = true; this._track = [localCursorPoint.slice()]; } } }, mousemove: function (e) { var localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY); resetCursor(this, e, localCursorPoint); if (this._dragging) { preventDefault(e); var eventParams = updateCoverByMouse(this, e, localCursorPoint, false); eventParams && trigger(this, eventParams); } }, mouseup: handleDragEnd //, // FIXME // in tooltip, globalout should not be triggered. // globalout: handleDragEnd }; function handleDragEnd(e) { if (this._dragging) { preventDefault(e); var localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY); var eventParams = updateCoverByMouse(this, e, localCursorPoint, true); this._dragging = false; this._track = []; this._creatingCover = null; // trigger event shoule be at final, after procedure will be nested. eventParams && trigger(this, eventParams); } } /** * key: brushType * @type {Object} */ var coverRenderers = { lineX: getLineRenderer(0), lineY: getLineRenderer(1), rect: { createCover: function (controller, brushOption) { return createBaseRectCover( curry$2( driftRect, function (range) { return range; }, function (range) { return range; } ), controller, brushOption, ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw'] ); }, getCreatingRange: function (localTrack) { var ends = getTrackEnds(localTrack); return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]); }, updateCoverShape: function (controller, cover, localRange, brushOption) { updateBaseRect(controller, cover, localRange, brushOption); }, updateCommon: updateCommon, contain: mainShapeContain }, polygon: { createCover: function (controller, brushOption) { var cover = new Group(); // Do not use graphic.Polygon because graphic.Polyline do not close the // border of the shape when drawing, which is a better experience for user. cover.add(new Polyline({ name: 'main', style: makeStyle(brushOption), silent: true })); return cover; }, getCreatingRange: function (localTrack) { return localTrack; }, endCreating: function (controller, cover) { cover.remove(cover.childAt(0)); // Use graphic.Polygon close the shape. cover.add(new Polygon({ name: 'main', draggable: true, drift: curry$2(driftPolygon, controller, cover), ondragend: curry$2(trigger, controller, {isEnd: true}) })); }, updateCoverShape: function (controller, cover, localRange, brushOption) { cover.childAt(0).setShape({ points: clipByPanel(controller, cover, localRange) }); }, updateCommon: updateCommon, contain: mainShapeContain } }; function getLineRenderer(xyIndex) { return { createCover: function (controller, brushOption) { return createBaseRectCover( curry$2( driftRect, function (range) { var rectRange = [range, [0, 100]]; xyIndex && rectRange.reverse(); return rectRange; }, function (rectRange) { return rectRange[xyIndex]; } ), controller, brushOption, [['w', 'e'], ['n', 's']][xyIndex] ); }, getCreatingRange: function (localTrack) { var ends = getTrackEnds(localTrack); var min = mathMin$6(ends[0][xyIndex], ends[1][xyIndex]); var max = mathMax$6(ends[0][xyIndex], ends[1][xyIndex]); return [min, max]; }, updateCoverShape: function (controller, cover, localRange, brushOption) { var otherExtent; // If brushWidth not specified, fit the panel. var panel = getPanelByCover(controller, cover); if (panel !== true && panel.getLinearBrushOtherExtent) { otherExtent = panel.getLinearBrushOtherExtent( xyIndex, controller._transform ); } else { var zr = controller._zr; otherExtent = [0, [zr.getWidth(), zr.getHeight()][1 - xyIndex]]; } var rectRange = [localRange, otherExtent]; xyIndex && rectRange.reverse(); updateBaseRect(controller, cover, rectRange, brushOption); }, updateCommon: updateCommon, contain: mainShapeContain }; } function makeRectPanelClipPath(rect) { rect = normalizeRect(rect); return function (localPoints, transform) { return clipPointsByRect(localPoints, rect); }; } function makeLinearBrushOtherExtent(rect, specifiedXYIndex) { rect = normalizeRect(rect); return function (xyIndex) { var idx = specifiedXYIndex != null ? specifiedXYIndex : xyIndex; var brushWidth = idx ? rect.width : rect.height; var base = idx ? rect.x : rect.y; return [base, base + (brushWidth || 0)]; }; } function makeRectIsTargetByCursor(rect, api, targetModel) { rect = normalizeRect(rect); return function (e, localCursorPoint, transform) { return rect.contain(localCursorPoint[0], localCursorPoint[1]) && !onIrrelevantElement(e, api, targetModel); }; } // Consider width/height is negative. function normalizeRect(rect) { return BoundingRect.create(rect); } var elementList = ['axisLine', 'axisTickLabel', 'axisName']; var AxisView$2 = extendComponentView({ type: 'parallelAxis', /** * @override */ init: function (ecModel, api) { AxisView$2.superApply(this, 'init', arguments); /** * @type {module:echarts/component/helper/BrushController} */ (this._brushController = new BrushController(api.getZr())) .on('brush', bind(this._onBrush, this)); }, /** * @override */ render: function (axisModel, ecModel, api, payload) { if (fromAxisAreaSelect(axisModel, ecModel, payload)) { return; } this.axisModel = axisModel; this.api = api; this.group.removeAll(); var oldAxisGroup = this._axisGroup; this._axisGroup = new Group(); this.group.add(this._axisGroup); if (!axisModel.get('show')) { return; } var coordSysModel = getCoordSysModel(axisModel, ecModel); var coordSys = coordSysModel.coordinateSystem; var areaSelectStyle = axisModel.getAreaSelectStyle(); var areaWidth = areaSelectStyle.width; var dim = axisModel.axis.dim; var axisLayout = coordSys.getAxisLayout(dim); var builderOpt = extend( {strokeContainThreshold: areaWidth}, axisLayout ); var axisBuilder = new AxisBuilder(axisModel, builderOpt); each$1(elementList, axisBuilder.add, axisBuilder); this._axisGroup.add(axisBuilder.getGroup()); this._refreshBrushController( builderOpt, areaSelectStyle, axisModel, coordSysModel, areaWidth, api ); var animationModel = (payload && payload.animation === false) ? null : axisModel; groupTransition(oldAxisGroup, this._axisGroup, animationModel); }, // /** // * @override // */ // updateVisual: function (axisModel, ecModel, api, payload) { // this._brushController && this._brushController // .updateCovers(getCoverInfoList(axisModel)); // }, _refreshBrushController: function ( builderOpt, areaSelectStyle, axisModel, coordSysModel, areaWidth, api ) { // After filtering, axis may change, select area needs to be update. var extent = axisModel.axis.getExtent(); var extentLen = extent[1] - extent[0]; var extra = Math.min(30, Math.abs(extentLen) * 0.1); // Arbitrary value. // width/height might be negative, which will be // normalized in BoundingRect. var rect = BoundingRect.create({ x: extent[0], y: -areaWidth / 2, width: extentLen, height: areaWidth }); rect.x -= extra; rect.width += 2 * extra; this._brushController .mount({ enableGlobalPan: true, rotation: builderOpt.rotation, position: builderOpt.position }) .setPanels([{ panelId: 'pl', clipPath: makeRectPanelClipPath(rect), isTargetByCursor: makeRectIsTargetByCursor(rect, api, coordSysModel), getLinearBrushOtherExtent: makeLinearBrushOtherExtent(rect, 0) }]) .enableBrush({ brushType: 'lineX', brushStyle: areaSelectStyle, removeOnClick: true }) .updateCovers(getCoverInfoList(axisModel)); }, _onBrush: function (coverInfoList, opt) { // Do not cache these object, because the mey be changed. var axisModel = this.axisModel; var axis = axisModel.axis; var intervals = map(coverInfoList, function (coverInfo) { return [ axis.coordToData(coverInfo.range[0], true), axis.coordToData(coverInfo.range[1], true) ]; }); // If realtime is true, action is not dispatched on drag end, because // the drag end emits the same params with the last drag move event, // and may have some delay when using touch pad. if (!axisModel.option.realtime === opt.isEnd || opt.removeOnClick) { // jshint ignore:line this.api.dispatchAction({ type: 'axisAreaSelect', parallelAxisId: axisModel.id, intervals: intervals }); } }, /** * @override */ dispose: function () { this._brushController.dispose(); } }); function fromAxisAreaSelect(axisModel, ecModel, payload) { return payload && payload.type === 'axisAreaSelect' && ecModel.findComponents( {mainType: 'parallelAxis', query: payload} )[0] === axisModel; } function getCoverInfoList(axisModel) { var axis = axisModel.axis; return map(axisModel.activeIntervals, function (interval) { return { brushType: 'lineX', panelId: 'pl', range: [ axis.dataToCoord(interval[0], true), axis.dataToCoord(interval[1], true) ] }; }); } function getCoordSysModel(axisModel, ecModel) { return ecModel.getComponent( 'parallel', axisModel.get('parallelIndex') ); } var CLICK_THRESHOLD = 5; // > 4 // Parallel view extendComponentView({ type: 'parallel', render: function (parallelModel, ecModel, api) { this._model = parallelModel; this._api = api; if (!this._handlers) { this._handlers = {}; each$1(handlers, function (handler, eventName) { api.getZr().on(eventName, this._handlers[eventName] = bind(handler, this)); }, this); } createOrUpdate( this, '_throttledDispatchExpand', parallelModel.get('axisExpandRate'), 'fixRate' ); }, dispose: function (ecModel, api) { each$1(this._handlers, function (handler, eventName) { api.getZr().off(eventName, handler); }); this._handlers = null; }, /** * @param {Object} [opt] If null, cancle the last action triggering for debounce. */ _throttledDispatchExpand: function (opt) { this._dispatchExpand(opt); }, _dispatchExpand: function (opt) { opt && this._api.dispatchAction( extend({type: 'parallelAxisExpand'}, opt) ); } }); var handlers = { mousedown: function (e) { if (checkTrigger(this, 'click')) { this._mouseDownPoint = [e.offsetX, e.offsetY]; } }, mouseup: function (e) { var mouseDownPoint = this._mouseDownPoint; if (checkTrigger(this, 'click') && mouseDownPoint) { var point = [e.offsetX, e.offsetY]; var dist = Math.pow(mouseDownPoint[0] - point[0], 2) + Math.pow(mouseDownPoint[1] - point[1], 2); if (dist > CLICK_THRESHOLD) { return; } var result = this._model.coordinateSystem.getSlidedAxisExpandWindow( [e.offsetX, e.offsetY] ); result.behavior !== 'none' && this._dispatchExpand({ axisExpandWindow: result.axisExpandWindow }); } this._mouseDownPoint = null; }, mousemove: function (e) { // Should do nothing when brushing. if (this._mouseDownPoint || !checkTrigger(this, 'mousemove')) { return; } var model = this._model; var result = model.coordinateSystem.getSlidedAxisExpandWindow( [e.offsetX, e.offsetY] ); var behavior = result.behavior; behavior === 'jump' && this._throttledDispatchExpand.debounceNextCall(model.get('axisExpandDebounce')); this._throttledDispatchExpand( behavior === 'none' ? null // Cancle the last trigger, in case that mouse slide out of the area quickly. : { axisExpandWindow: result.axisExpandWindow, // Jumping uses animation, and sliding suppresses animation. animation: behavior === 'jump' ? null : false } ); } }; function checkTrigger(view, triggerOn) { var model = view._model; return model.get('axisExpandable') && model.get('axisExpandTriggerOn') === triggerOn; } registerPreprocessor(parallelPreprocessor); SeriesModel.extend({ type: 'series.parallel', dependencies: ['parallel'], visualColorAccessPath: 'lineStyle.color', getInitialData: function (option, ecModel) { // Anication is forbiden in progressive data mode. if (this.option.progressive) { this.option.animation = false; } var source = this.getSource(); setEncodeAndDimensions(source, this); return createListFromArray(source, this); }, /** * User can get data raw indices on 'axisAreaSelected' event received. * * @public * @param {string} activeState 'active' or 'inactive' or 'normal' * @return {Array.<number>} Raw indices */ getRawIndicesByActiveState: function (activeState) { var coordSys = this.coordinateSystem; var data = this.getData(); var indices = []; coordSys.eachActiveState(data, function (theActiveState, dataIndex) { if (activeState === theActiveState) { indices.push(data.getRawIndex(dataIndex)); } }); return indices; }, defaultOption: { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 coordinateSystem: 'parallel', parallelIndex: 0, label: { show: false }, inactiveOpacity: 0.05, activeOpacity: 1, lineStyle: { width: 1, opacity: 0.45, type: 'solid' }, emphasis: { label: { show: false } }, progressive: false, // 100 smooth: false, animationEasing: 'linear' } }); function setEncodeAndDimensions(source, seriesModel) { // The mapping of parallelAxis dimension to data dimension can // be specified in parallelAxis.option.dim. For example, if // parallelAxis.option.dim is 'dim3', it mapping to the third // dimension of data. But `data.encode` has higher priority. // Moreover, parallelModel.dimension should not be regarded as data // dimensions. Consider dimensions = ['dim4', 'dim2', 'dim6']; if (source.encodeDefine) { return; } var parallelModel = seriesModel.ecModel.getComponent( 'parallel', seriesModel.get('parallelIndex') ); if (!parallelModel) { return; } var encodeDefine = source.encodeDefine = createHashMap(); each$1(parallelModel.dimensions, function (axisDim) { var dataDimIndex = convertDimNameToNumber(axisDim); encodeDefine.set(axisDim, dataDimIndex); }); } function convertDimNameToNumber(dimName) { return +dimName.replace('dim', ''); } var SMOOTH = 0.3; var ParallelView = Chart.extend({ type: 'parallel', init: function () { /** * @type {module:zrender/container/Group} * @private */ this._dataGroup = new Group(); this.group.add(this._dataGroup); /** * @type {module:echarts/data/List} */ this._data; }, /** * @override */ render: function (seriesModel, ecModel, api, payload) { this._renderForNormal(seriesModel, payload); // this[ // seriesModel.option.progressive // ? '_renderForProgressive' // : '_renderForNormal' // ](seriesModel); }, dispose: function () {}, /** * @private */ _renderForNormal: function (seriesModel, payload) { var dataGroup = this._dataGroup; var data = seriesModel.getData(); var oldData = this._data; var coordSys = seriesModel.coordinateSystem; var dimensions = coordSys.dimensions; var option = seriesModel.option; var smooth = option.smooth ? SMOOTH : null; // Consider switch between progressive and not. // oldData && oldData.__plProgressive && dataGroup.removeAll(); data.diff(oldData) .add(add) .update(update) .remove(remove) .execute(); // Update style updateElCommon(data, smooth); // First create if (!this._data) { var clipPath = createGridClipShape$1( coordSys, seriesModel, function () { // Callback will be invoked immediately if there is no animation setTimeout(function () { dataGroup.removeClipPath(); }); } ); dataGroup.setClipPath(clipPath); } this._data = data; function add(newDataIndex) { addEl(data, dataGroup, newDataIndex, dimensions, coordSys, null, smooth); } function update(newDataIndex, oldDataIndex) { var line = oldData.getItemGraphicEl(oldDataIndex); var points = createLinePoints(data, newDataIndex, dimensions, coordSys); data.setItemGraphicEl(newDataIndex, line); var animationModel = (payload && payload.animation === false) ? null : seriesModel; updateProps(line, {shape: {points: points}}, animationModel, newDataIndex); } function remove(oldDataIndex) { var line = oldData.getItemGraphicEl(oldDataIndex); dataGroup.remove(line); } }, /** * @private */ // _renderForProgressive: function (seriesModel) { // var dataGroup = this._dataGroup; // var data = seriesModel.getData(); // var oldData = this._data; // var coordSys = seriesModel.coordinateSystem; // var dimensions = coordSys.dimensions; // var option = seriesModel.option; // var progressive = option.progressive; // var smooth = option.smooth ? SMOOTH : null; // // In progressive animation is disabled, so use simple data diff, // // which effects performance less. // // (Typically performance for data with length 7000+ like: // // simpleDiff: 60ms, addEl: 184ms, // // in RMBP 2.4GHz intel i7, OSX 10.9 chrome 50.0.2661.102 (64-bit)) // if (simpleDiff(oldData, data, dimensions)) { // dataGroup.removeAll(); // data.each(function (dataIndex) { // addEl(data, dataGroup, dataIndex, dimensions, coordSys); // }); // } // updateElCommon(data, progressive, smooth); // // Consider switch between progressive and not. // data.__plProgressive = true; // this._data = data; // }, /** * @override */ remove: function () { this._dataGroup && this._dataGroup.removeAll(); this._data = null; } }); function createGridClipShape$1(coordSys, seriesModel, cb) { var parallelModel = coordSys.model; var rect = coordSys.getRect(); var rectEl = new Rect({ shape: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }); var dim = parallelModel.get('layout') === 'horizontal' ? 'width' : 'height'; rectEl.setShape(dim, 0); initProps(rectEl, { shape: { width: rect.width, height: rect.height } }, seriesModel, cb); return rectEl; } function createLinePoints(data, dataIndex, dimensions, coordSys) { var points = []; for (var i = 0; i < dimensions.length; i++) { var dimName = dimensions[i]; var value = data.get(data.mapDimension(dimName), dataIndex); if (!isEmptyValue(value, coordSys.getAxis(dimName).type)) { points.push(coordSys.dataToPoint(value, dimName)); } } return points; } function addEl(data, dataGroup, dataIndex, dimensions, coordSys) { var points = createLinePoints(data, dataIndex, dimensions, coordSys); var line = new Polyline({ shape: {points: points}, silent: true, z2: 10 }); dataGroup.add(line); data.setItemGraphicEl(dataIndex, line); } function updateElCommon(data, smooth) { var seriesStyleModel = data.hostModel.getModel('lineStyle'); var lineStyle = seriesStyleModel.getLineStyle(); data.eachItemGraphicEl(function (line, dataIndex) { if (data.hasItemOption) { var itemModel = data.getItemModel(dataIndex); var lineStyleModel = itemModel.getModel('lineStyle', seriesStyleModel); lineStyle = lineStyleModel.getLineStyle(['color', 'stroke']); } line.useStyle(extend(lineStyle, { fill: null, // lineStyle.color have been set to itemVisual in module:echarts/visual/seriesColor. stroke: data.getItemVisual(dataIndex, 'color'), // lineStyle.opacity have been set to itemVisual in parallelVisual. opacity: data.getItemVisual(dataIndex, 'opacity') })); line.shape.smooth = smooth; }); } // function simpleDiff(oldData, newData, dimensions) { // var oldLen; // if (!oldData // || !oldData.__plProgressive // || (oldLen = oldData.count()) !== newData.count() // ) { // return true; // } // var dimLen = dimensions.length; // for (var i = 0; i < oldLen; i++) { // for (var j = 0; j < dimLen; j++) { // if (oldData.get(dimensions[j], i) !== newData.get(dimensions[j], i)) { // return true; // } // } // } // return false; // } // FIXME // 公用方法? function isEmptyValue(val, axisType) { return axisType === 'category' ? val == null : (val == null || isNaN(val)); // axisType === 'value' } var opacityAccessPath$1 = ['lineStyle', 'normal', 'opacity']; var parallelVisual = function (ecModel) { ecModel.eachSeriesByType('parallel', function (seriesModel) { var itemStyleModel = seriesModel.getModel('itemStyle'); var lineStyleModel = seriesModel.getModel('lineStyle'); var globalColors = ecModel.get('color'); var color = lineStyleModel.get('color') || itemStyleModel.get('color') || globalColors[seriesModel.seriesIndex % globalColors.length]; var inactiveOpacity = seriesModel.get('inactiveOpacity'); var activeOpacity = seriesModel.get('activeOpacity'); var lineStyle = seriesModel.getModel('lineStyle').getLineStyle(); var coordSys = seriesModel.coordinateSystem; var data = seriesModel.getData(); var opacityMap = { normal: lineStyle.opacity, active: activeOpacity, inactive: inactiveOpacity }; coordSys.eachActiveState(data, function (activeState, dataIndex) { var itemModel = data.getItemModel(dataIndex); var opacity = opacityMap[activeState]; if (activeState === 'normal') { var itemOpacity = itemModel.get(opacityAccessPath$1, true); itemOpacity != null && (opacity = itemOpacity); } data.setItemVisual(dataIndex, 'opacity', opacity); }); data.setVisual('color', color); }); }; registerVisual(parallelVisual); /** * @file Get initial data and define sankey view's series model * @author Deqing Li(annong035@gmail.com) */ var SankeySeries = SeriesModel.extend({ type: 'series.sankey', layoutInfo: null, /** * Init a graph data structure from data in option series * * @param {Object} option the object used to config echarts view * @return {module:echarts/data/List} storage initial data */ getInitialData: function (option) { var links = option.edges || option.links; var nodes = option.data || option.nodes; if (nodes && links) { var graph = createGraphFromNodeEdge(nodes, links, this, true); return graph.data; } }, /** * Return the graphic data structure * * @return {module:echarts/data/Graph} graphic data structure */ getGraph: function () { return this.getData().graph; }, /** * Get edge data of graphic data structure * * @return {module:echarts/data/List} data structure of list */ getEdgeData: function () { return this.getGraph().edgeData; }, /** * @override */ formatTooltip: function (dataIndex, multipleSeries, dataType) { // dataType === 'node' or empty do not show tooltip by default if (dataType === 'edge') { var params = this.getDataParams(dataIndex, dataType); var rawDataOpt = params.data; var html = rawDataOpt.source + ' -- ' + rawDataOpt.target; if (params.value) { html += ' : ' + params.value; } return encodeHTML(html); } return SankeySeries.superCall(this, 'formatTooltip', dataIndex, multipleSeries); }, defaultOption: { zlevel: 0, z: 2, coordinateSystem: 'view', layout: null, // the position of the whole view left: '5%', top: '5%', right: '20%', bottom: '5%', // the dx of the node nodeWidth: 20, // the vertical distance between two nodes nodeGap: 8, // the number of iterations to change the position of the node layoutIterations: 32, label: { show: true, position: 'right', color: '#000', fontSize: 12 }, itemStyle: { borderWidth: 1, borderColor: '#333' }, lineStyle: { color: '#314656', opacity: 0.2, curveness: 0.5 }, emphasis: { label: { show: true }, lineStyle: { opacity: 0.6 } }, animationEasing: 'linear', animationDuration: 1000 } }); /** * @file The file used to draw sankey view * @author Deqing Li(annong035@gmail.com) */ var SankeyShape = extendShape({ shape: { x1: 0, y1: 0, x2: 0, y2: 0, cpx1: 0, cpy1: 0, cpx2: 0, cpy2: 0, extent: 0 }, buildPath: function (ctx, shape) { var halfExtent = shape.extent / 2; ctx.moveTo(shape.x1, shape.y1 - halfExtent); ctx.bezierCurveTo( shape.cpx1, shape.cpy1 - halfExtent, shape.cpx2, shape.cpy2 - halfExtent, shape.x2, shape.y2 - halfExtent ); ctx.lineTo(shape.x2, shape.y2 + halfExtent); ctx.bezierCurveTo( shape.cpx2, shape.cpy2 + halfExtent, shape.cpx1, shape.cpy1 + halfExtent, shape.x1, shape.y1 + halfExtent ); ctx.closePath(); } }); extendChartView({ type: 'sankey', /** * @private * @type {module:echarts/chart/sankey/SankeySeries} */ _model: null, render: function (seriesModel, ecModel, api) { var graph = seriesModel.getGraph(); var group = this.group; var layoutInfo = seriesModel.layoutInfo; var nodeData = seriesModel.getData(); var edgeData = seriesModel.getData('edge'); this._model = seriesModel; group.removeAll(); group.attr('position', [layoutInfo.x, layoutInfo.y]); // generate a bezire Curve for each edge graph.eachEdge(function (edge) { var curve = new SankeyShape(); curve.dataIndex = edge.dataIndex; curve.seriesIndex = seriesModel.seriesIndex; curve.dataType = 'edge'; var lineStyleModel = edge.getModel('lineStyle'); var curvature = lineStyleModel.get('curveness'); var n1Layout = edge.node1.getLayout(); var n2Layout = edge.node2.getLayout(); var edgeLayout = edge.getLayout(); curve.shape.extent = Math.max(1, edgeLayout.dy); var x1 = n1Layout.x + n1Layout.dx; var y1 = n1Layout.y + edgeLayout.sy + edgeLayout.dy / 2; var x2 = n2Layout.x; var y2 = n2Layout.y + edgeLayout.ty + edgeLayout.dy / 2; var cpx1 = x1 * (1 - curvature) + x2 * curvature; var cpy1 = y1; var cpx2 = x1 * curvature + x2 * (1 - curvature); var cpy2 = y2; curve.setShape({ x1: x1, y1: y1, x2: x2, y2: y2, cpx1: cpx1, cpy1: cpy1, cpx2: cpx2, cpy2: cpy2 }); curve.setStyle(lineStyleModel.getItemStyle()); // Special color, use source node color or target node color switch (curve.style.fill) { case 'source': curve.style.fill = edge.node1.getVisual('color'); break; case 'target': curve.style.fill = edge.node2.getVisual('color'); break; } setHoverStyle(curve, edge.getModel('emphasis.lineStyle').getItemStyle()); group.add(curve); edgeData.setItemGraphicEl(edge.dataIndex, curve); }); // generate a rect for each node graph.eachNode(function (node) { var layout = node.getLayout(); var itemModel = node.getModel(); var labelModel = itemModel.getModel('label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var rect = new Rect({ shape: { x: layout.x, y: layout.y, width: node.getLayout().dx, height: node.getLayout().dy }, style: itemModel.getModel('itemStyle').getItemStyle() }); var hoverStyle = node.getModel('emphasis.itemStyle').getItemStyle(); setLabelStyle( rect.style, hoverStyle, labelModel, labelHoverModel, { labelFetcher: seriesModel, labelDataIndex: node.dataIndex, defaultText: node.id, isRectText: true } ); rect.setStyle('fill', node.getVisual('color')); setHoverStyle(rect, hoverStyle); group.add(rect); nodeData.setItemGraphicEl(node.dataIndex, rect); rect.dataType = 'node'; }); if (!this._data && seriesModel.get('animation')) { group.setClipPath(createGridClipShape$2(group.getBoundingRect(), seriesModel, function () { group.removeClipPath(); })); } this._data = seriesModel.getData(); }, dispose: function () {} }); // add animation to the view function createGridClipShape$2(rect, seriesModel, cb) { var rectEl = new Rect({ shape: { x: rect.x - 10, y: rect.y - 10, width: 0, height: rect.height + 20 } }); initProps(rectEl, { shape: { width: rect.width + 20, height: rect.height + 20 } }, seriesModel, cb); return rectEl; } /** * nest helper used to group by the array. * can specified the keys and sort the keys. */ function nest() { var keysFunction = []; var sortKeysFunction = []; /** * map an Array into the mapObject. * @param {Array} array * @param {number} depth */ function map$$1(array, depth) { if (depth >= keysFunction.length) { return array; } var i = -1; var n = array.length; var keyFunction = keysFunction[depth++]; var mapObject = {}; var valuesByKey = {}; while (++i < n) { var keyValue = keyFunction(array[i]); var values = valuesByKey[keyValue]; if (values) { values.push(array[i]); } else { valuesByKey[keyValue] = [array[i]]; } } each$1(valuesByKey, function (value, key) { mapObject[key] = map$$1(value, depth); }); return mapObject; } /** * transform the Map Object to multidimensional Array * @param {Object} map * @param {number} depth */ function entriesMap(mapObject, depth) { if (depth >= keysFunction.length) { return mapObject; } var array = []; var sortKeyFunction = sortKeysFunction[depth++]; each$1(mapObject, function (value, key) { array.push({ key: key, values: entriesMap(value, depth) }); }); if (sortKeyFunction) { return array.sort(function (a, b) { return sortKeyFunction(a.key, b.key); }); } else { return array; } } return { /** * specified the key to groupby the arrays. * users can specified one more keys. * @param {Function} d */ key: function (d) { keysFunction.push(d); return this; }, /** * specified the comparator to sort the keys * @param {Function} order */ sortKeys: function (order) { sortKeysFunction[keysFunction.length - 1] = order; return this; }, /** * the array to be grouped by. * @param {Array} array */ entries: function (array) { return entriesMap(map$$1(array, 0), 0); } }; } /** * @file The layout algorithm of sankey view * @author Deqing Li(annong035@gmail.com) */ var sankeyLayout = function (ecModel, api, payload) { ecModel.eachSeriesByType('sankey', function (seriesModel) { var nodeWidth = seriesModel.get('nodeWidth'); var nodeGap = seriesModel.get('nodeGap'); var layoutInfo = getViewRect$3(seriesModel, api); seriesModel.layoutInfo = layoutInfo; var width = layoutInfo.width; var height = layoutInfo.height; var graph = seriesModel.getGraph(); var nodes = graph.nodes; var edges = graph.edges; computeNodeValues(nodes); var filteredNodes = filter(nodes, function (node) { return node.getLayout().value === 0; }); var iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations'); layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations); }); }; /** * Get the layout position of the whole view * * @param {module:echarts/model/Series} seriesModel the model object of sankey series * @param {module:echarts/ExtensionAPI} api provide the API list that the developer can call * @return {module:zrender/core/BoundingRect} size of rect to draw the sankey view */ function getViewRect$3(seriesModel, api) { return getLayoutRect( seriesModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() } ); } function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations) { computeNodeBreadths(nodes, nodeWidth, width); computeNodeDepths(nodes, edges, height, nodeGap, iterations); computeEdgeDepths(nodes); } /** * Compute the value of each node by summing the associated edge's value * * @param {module:echarts/data/Graph~Node} nodes node of sankey view */ function computeNodeValues(nodes) { each$1(nodes, function (node) { var value1 = sum(node.outEdges, getEdgeValue); var value2 = sum(node.inEdges, getEdgeValue); var value = Math.max(value1, value2); node.setLayout({value: value}, true); }); } /** * Compute the x-position for each node * * @param {module:echarts/data/Graph~Node} nodes node of sankey view * @param {number} nodeWidth the dx of the node * @param {number} width the whole width of the area to draw the view */ function computeNodeBreadths(nodes, nodeWidth, width) { var remainNodes = nodes; var nextNode = null; var x = 0; var kx = 0; while (remainNodes.length) { nextNode = []; for (var i = 0, len = remainNodes.length; i < len; i++) { var node = remainNodes[i]; node.setLayout({x: x}, true); node.setLayout({dx: nodeWidth}, true); for (var j = 0, lenj = node.outEdges.length; j < lenj; j++) { nextNode.push(node.outEdges[j].node2); } } remainNodes = nextNode; ++x; } moveSinksRight(nodes, x); kx = (width - nodeWidth) / (x - 1); scaleNodeBreadths(nodes, kx); } /** * All the node without outEgdes are assigned maximum x-position and * be aligned in the last column. * * @param {module:echarts/data/Graph~Node} nodes node of sankey view * @param {number} x value (x-1) use to assign to node without outEdges * as x-position */ function moveSinksRight(nodes, x) { each$1(nodes, function (node) { if (!node.outEdges.length) { node.setLayout({x: x - 1}, true); } }); } /** * Scale node x-position to the width * * @param {module:echarts/data/Graph~Node} nodes node of sankey view * @param {number} kx multiple used to scale nodes */ function scaleNodeBreadths(nodes, kx) { each$1(nodes, function (node) { var nodeX = node.getLayout().x * kx; node.setLayout({x: nodeX}, true); }); } /** * Using Gauss-Seidel iterations method to compute the node depth(y-position) * * @param {module:echarts/data/Graph~Node} nodes node of sankey view * @param {module:echarts/data/Graph~Edge} edges edge of sankey view * @param {number} height the whole height of the area to draw the view * @param {number} nodeGap the vertical distance between two nodes * in the same column. * @param {number} iterations the number of iterations for the algorithm */ function computeNodeDepths(nodes, edges, height, nodeGap, iterations) { var nodesByBreadth = nest() .key(function (d) { return d.getLayout().x; }) .sortKeys(ascending) .entries(nodes) .map(function (d) { return d.values; }); initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap); resolveCollisions(nodesByBreadth, nodeGap, height); for (var alpha = 1; iterations > 0; iterations--) { // 0.99 is a experience parameter, ensure that each iterations of // changes as small as possible. alpha *= 0.99; relaxRightToLeft(nodesByBreadth, alpha); resolveCollisions(nodesByBreadth, nodeGap, height); relaxLeftToRight(nodesByBreadth, alpha); resolveCollisions(nodesByBreadth, nodeGap, height); } } /** * Compute the original y-position for each node * * @param {module:echarts/data/Graph~Node} nodes node of sankey view * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth * group by the array of all sankey nodes based on the nodes x-position. * @param {module:echarts/data/Graph~Edge} edges edge of sankey view * @param {number} height the whole height of the area to draw the view * @param {number} nodeGap the vertical distance between two nodes */ function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) { var kyArray = []; each$1(nodesByBreadth, function (nodes) { var n = nodes.length; var sum = 0; each$1(nodes, function (node) { sum += node.getLayout().value; }); var ky = (height - (n - 1) * nodeGap) / sum; kyArray.push(ky); }); kyArray.sort(function (a, b) { return a - b; }); var ky0 = kyArray[0]; each$1(nodesByBreadth, function (nodes) { each$1(nodes, function (node, i) { node.setLayout({y: i}, true); var nodeDy = node.getLayout().value * ky0; node.setLayout({dy: nodeDy}, true); }); }); each$1(edges, function (edge) { var edgeDy = +edge.getValue() * ky0; edge.setLayout({dy: edgeDy}, true); }); } /** * Resolve the collision of initialized depth (y-position) * * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth * group by the array of all sankey nodes based on the nodes x-position. * @param {number} nodeGap the vertical distance between two nodes * @param {number} height the whole height of the area to draw the view */ function resolveCollisions(nodesByBreadth, nodeGap, height) { each$1(nodesByBreadth, function (nodes) { var node; var dy; var y0 = 0; var n = nodes.length; var i; nodes.sort(ascendingDepth); for (i = 0; i < n; i++) { node = nodes[i]; dy = y0 - node.getLayout().y; if (dy > 0) { var nodeY = node.getLayout().y + dy; node.setLayout({y: nodeY}, true); } y0 = node.getLayout().y + node.getLayout().dy + nodeGap; } // if the bottommost node goes outside the bounds, push it back up dy = y0 - nodeGap - height; if (dy > 0) { var nodeY = node.getLayout().y - dy; node.setLayout({y: nodeY}, true); y0 = node.getLayout().y; for (i = n - 2; i >= 0; --i) { node = nodes[i]; dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0; if (dy > 0) { nodeY = node.getLayout().y - dy; node.setLayout({y: nodeY}, true); } y0 = node.getLayout().y; } } }); } /** * Change the y-position of the nodes, except most the right side nodes * * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth * group by the array of all sankey nodes based on the node x-position. * @param {number} alpha parameter used to adjust the nodes y-position */ function relaxRightToLeft(nodesByBreadth, alpha) { each$1(nodesByBreadth.slice().reverse(), function (nodes) { each$1(nodes, function (node) { if (node.outEdges.length) { var y = sum(node.outEdges, weightedTarget) / sum(node.outEdges, getEdgeValue); var nodeY = node.getLayout().y + (y - center$1(node)) * alpha; node.setLayout({y: nodeY}, true); } }); }); } function weightedTarget(edge) { return center$1(edge.node2) * edge.getValue(); } /** * Change the y-position of the nodes, except most the left side nodes * * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth * group by the array of all sankey nodes based on the node x-position. * @param {number} alpha parameter used to adjust the nodes y-position */ function relaxLeftToRight(nodesByBreadth, alpha) { each$1(nodesByBreadth, function (nodes) { each$1(nodes, function (node) { if (node.inEdges.length) { var y = sum(node.inEdges, weightedSource) / sum(node.inEdges, getEdgeValue); var nodeY = node.getLayout().y + (y - center$1(node)) * alpha; node.setLayout({y: nodeY}, true); } }); }); } function weightedSource(edge) { return center$1(edge.node1) * edge.getValue(); } /** * Compute the depth(y-position) of each edge * * @param {module:echarts/data/Graph~Node} nodes node of sankey view */ function computeEdgeDepths(nodes) { each$1(nodes, function (node) { node.outEdges.sort(ascendingTargetDepth); node.inEdges.sort(ascendingSourceDepth); }); each$1(nodes, function (node) { var sy = 0; var ty = 0; each$1(node.outEdges, function (edge) { edge.setLayout({sy: sy}, true); sy += edge.getLayout().dy; }); each$1(node.inEdges, function (edge) { edge.setLayout({ty: ty}, true); ty += edge.getLayout().dy; }); }); } function ascendingTargetDepth(a, b) { return a.node2.getLayout().y - b.node2.getLayout().y; } function ascendingSourceDepth(a, b) { return a.node1.getLayout().y - b.node1.getLayout().y; } function sum(array, f) { var sum = 0; var len = array.length; var i = -1; while (++i < len) { var value = +f.call(array, array[i], i); if (!isNaN(value)) { sum += value; } } return sum; } function center$1(node) { return node.getLayout().y + node.getLayout().dy / 2; } function ascendingDepth(a, b) { return a.getLayout().y - b.getLayout().y; } function ascending(a, b) { return a < b ? -1 : a > b ? 1 : a === b ? 0 : NaN; } function getEdgeValue(edge) { return edge.getValue(); } /** * @file Visual encoding for sankey view * @author Deqing Li(annong035@gmail.com) */ var sankeyVisual = function (ecModel, payload) { ecModel.eachSeriesByType('sankey', function (seriesModel) { var graph = seriesModel.getGraph(); var nodes = graph.nodes; nodes.sort(function (a, b) { return a.getLayout().value - b.getLayout().value; }); var minValue = nodes[0].getLayout().value; var maxValue = nodes[nodes.length - 1].getLayout().value; each$1(nodes, function (node) { var mapping = new VisualMapping({ type: 'color', mappingMethod: 'linear', dataExtent: [minValue, maxValue], visual: seriesModel.get('color') }); var mapValueToColor = mapping.mapValueToVisual(node.getLayout().value); node.setVisual('color', mapValueToColor); // If set itemStyle.normal.color var itemModel = node.getModel(); var customColor = itemModel.get('itemStyle.color'); if (customColor != null) { node.setVisual('color', customColor); } }); }); }; registerLayout(sankeyLayout); registerVisual(sankeyVisual); /** * @module echarts/chart/helper/Symbol */ var WhiskerPath = Path.extend({ type: 'whiskerInBox', shape: {}, buildPath: function (ctx, shape) { for (var i in shape) { if (shape.hasOwnProperty(i) && i.indexOf('ends') === 0) { var pts = shape[i]; ctx.moveTo(pts[0][0], pts[0][1]); ctx.lineTo(pts[1][0], pts[1][1]); } } } }); /** * @constructor * @alias {module:echarts/chart/helper/WhiskerBox} * @param {module:echarts/data/List} data * @param {number} idx * @param {Function} styleUpdater * @param {boolean} isInit * @extends {module:zrender/graphic/Group} */ function WhiskerBox(data, idx, styleUpdater, isInit) { Group.call(this); /** * @type {number} * @readOnly */ this.bodyIndex; /** * @type {number} * @readOnly */ this.whiskerIndex; /** * @type {Function} */ this.styleUpdater = styleUpdater; this._createContent(data, idx, isInit); this.updateData(data, idx, isInit); /** * Last series model. * @type {module:echarts/model/Series} */ this._seriesModel; } var whiskerBoxProto = WhiskerBox.prototype; whiskerBoxProto._createContent = function (data, idx, isInit) { var itemLayout = data.getItemLayout(idx); var constDim = itemLayout.chartLayout === 'horizontal' ? 1 : 0; var count = 0; // Whisker element. this.add(new Polygon({ shape: { points: isInit ? transInit(itemLayout.bodyEnds, constDim, itemLayout) : itemLayout.bodyEnds }, style: {strokeNoScale: true}, z2: 100 })); this.bodyIndex = count++; // Box element. var whiskerEnds = map(itemLayout.whiskerEnds, function (ends) { return isInit ? transInit(ends, constDim, itemLayout) : ends; }); this.add(new WhiskerPath({ shape: makeWhiskerEndsShape(whiskerEnds), style: {strokeNoScale: true}, z2: 100 })); this.whiskerIndex = count++; }; function transInit(points, dim, itemLayout) { return map(points, function (point) { point = point.slice(); point[dim] = itemLayout.initBaseline; return point; }); } function makeWhiskerEndsShape(whiskerEnds) { // zr animation only support 2-dim array. var shape = {}; each$1(whiskerEnds, function (ends, i) { shape['ends' + i] = ends; }); return shape; } /** * Update symbol properties * @param {module:echarts/data/List} data * @param {number} idx */ whiskerBoxProto.updateData = function (data, idx, isInit) { var seriesModel = this._seriesModel = data.hostModel; var itemLayout = data.getItemLayout(idx); var updateMethod = graphic[isInit ? 'initProps' : 'updateProps']; // this.childAt(this.bodyIndex).stopAnimation(true); // this.childAt(this.whiskerIndex).stopAnimation(true); updateMethod( this.childAt(this.bodyIndex), {shape: {points: itemLayout.bodyEnds}}, seriesModel, idx ); updateMethod( this.childAt(this.whiskerIndex), {shape: makeWhiskerEndsShape(itemLayout.whiskerEnds)}, seriesModel, idx ); this.styleUpdater.call(null, this, data, idx); }; inherits(WhiskerBox, Group); /** * @constructor * @alias module:echarts/chart/helper/WhiskerBoxDraw */ function WhiskerBoxDraw(styleUpdater) { this.group = new Group(); this.styleUpdater = styleUpdater; } var whiskerBoxDrawProto = WhiskerBoxDraw.prototype; /** * Update symbols draw by new data * @param {module:echarts/data/List} data */ whiskerBoxDrawProto.updateData = function (data) { var group = this.group; var oldData = this._data; var styleUpdater = this.styleUpdater; // There is no old data only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!this._data) { group.removeAll(); } data.diff(oldData) .add(function (newIdx) { if (data.hasValue(newIdx)) { var symbolEl = new WhiskerBox(data, newIdx, styleUpdater, true); data.setItemGraphicEl(newIdx, symbolEl); group.add(symbolEl); } }) .update(function (newIdx, oldIdx) { var symbolEl = oldData.getItemGraphicEl(oldIdx); // Empty data if (!data.hasValue(newIdx)) { group.remove(symbolEl); return; } if (!symbolEl) { symbolEl = new WhiskerBox(data, newIdx, styleUpdater); } else { symbolEl.updateData(data, newIdx); } // Add back group.add(symbolEl); data.setItemGraphicEl(newIdx, symbolEl); }) .remove(function (oldIdx) { var el = oldData.getItemGraphicEl(oldIdx); el && group.remove(el); }) .execute(); this._data = data; }; whiskerBoxDrawProto.incrementalPrepareUpdate = function (seriesModel, ecModel, api) { this.group.removeAll(); this._data = null; }; whiskerBoxDrawProto.incrementalUpdate = function (params, seriesModel, ecModel, api) { var data = seriesModel.getData(); for (var idx = params.start; idx < params.end; idx++) { var symbolEl = new WhiskerBox(data, idx, this.styleUpdater, true); symbolEl.incremental = true; this.group.add(symbolEl); } }; /** * Remove symbols. * @param {module:echarts/data/List} data */ whiskerBoxDrawProto.remove = function () { var group = this.group; var data = this._data; this._data = null; data && data.eachItemGraphicEl(function (el) { el && group.remove(el); }); }; var seriesModelMixin = { /** * @private * @type {string} */ _baseAxisDim: null, /** * @override */ getInitialData: function (option, ecModel) { // When both types of xAxis and yAxis are 'value', layout is // needed to be specified by user. Otherwise, layout can be // judged by which axis is category. var ordinalMeta; var xAxisModel = ecModel.getComponent('xAxis', this.get('xAxisIndex')); var yAxisModel = ecModel.getComponent('yAxis', this.get('yAxisIndex')); var xAxisType = xAxisModel.get('type'); var yAxisType = yAxisModel.get('type'); var addOrdinal; // FIXME // 考虑时间轴 if (xAxisType === 'category') { option.layout = 'horizontal'; ordinalMeta = xAxisModel.getOrdinalMeta(); addOrdinal = true; } else if (yAxisType === 'category') { option.layout = 'vertical'; ordinalMeta = yAxisModel.getOrdinalMeta(); addOrdinal = true; } else { option.layout = option.layout || 'horizontal'; } var coordDims = ['x', 'y']; var baseAxisDimIndex = option.layout === 'horizontal' ? 0 : 1; var baseAxisDim = this._baseAxisDim = coordDims[baseAxisDimIndex]; var otherAxisDim = coordDims[1 - baseAxisDimIndex]; var axisModels = [xAxisModel, yAxisModel]; var baseAxisType = axisModels[baseAxisDimIndex].get('type'); var otherAxisType = axisModels[1 - baseAxisDimIndex].get('type'); var data = option.data; // ??? FIXME make a stage to perform data transfrom. // MUST create a new data, consider setOption({}) again. if (data && addOrdinal) { var newOptionData = []; each$1(data, function (item, index) { var newItem; if (item.value && isArray(item.value)) { newItem = item.value.slice(); item.value.unshift(index); } else if (isArray(item)) { newItem = item.slice(); item.unshift(index); } else { newItem = item; } newOptionData.push(newItem); }); option.data = newOptionData; } var defaultValueDimensions = this.defaultValueDimensions; return createListSimply( this, { coordDimensions: [{ name: baseAxisDim, type: getDimensionTypeByAxis(baseAxisType), ordinalMeta: ordinalMeta, otherDims: { tooltip: false, itemName: 0 }, dimsDef: ['base'] }, { name: otherAxisDim, type: getDimensionTypeByAxis(otherAxisType), dimsDef: defaultValueDimensions.slice() }], dimensionsCount: defaultValueDimensions.length + 1 } ); }, /** * If horizontal, base axis is x, otherwise y. * @override */ getBaseAxis: function () { var dim = this._baseAxisDim; return this.ecModel.getComponent(dim + 'Axis', this.get(dim + 'AxisIndex')).axis; } }; var viewMixin = { init: function () { /** * Old data. * @private * @type {module:echarts/chart/helper/WhiskerBoxDraw} */ var whiskerBoxDraw = this._whiskerBoxDraw = new WhiskerBoxDraw( this.getStyleUpdater() ); this.group.add(whiskerBoxDraw.group); }, render: function (seriesModel, ecModel, api) { this._whiskerBoxDraw.updateData(seriesModel.getData()); }, incrementalPrepareRender: function (seriesModel, ecModel, api) { this._whiskerBoxDraw.incrementalPrepareUpdate(seriesModel, ecModel, api); }, incrementalRender: function (params, seriesModel, ecModel, api) { this._whiskerBoxDraw.incrementalUpdate(params, seriesModel, ecModel, api); }, remove: function (ecModel) { this._whiskerBoxDraw.remove(); } }; var BoxplotSeries = SeriesModel.extend({ type: 'series.boxplot', dependencies: ['xAxis', 'yAxis', 'grid'], // TODO // box width represents group size, so dimension should have 'size'. /** * @see <https://en.wikipedia.org/wiki/Box_plot> * The meanings of 'min' and 'max' depend on user, * and echarts do not need to know it. * @readOnly */ defaultValueDimensions: ['min', 'Q1', 'median', 'Q3', 'max'], /** * @type {Array.<string>} * @readOnly */ dimensions: null, /** * @override */ defaultOption: { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 coordinateSystem: 'cartesian2d', legendHoverLink: true, hoverAnimation: true, // xAxisIndex: 0, // yAxisIndex: 0, layout: null, // 'horizontal' or 'vertical' boxWidth: [7, 50], // [min, max] can be percent of band width. itemStyle: { color: '#fff', borderWidth: 1 }, emphasis: { itemStyle: { borderWidth: 2, shadowBlur: 5, shadowOffsetX: 2, shadowOffsetY: 2, shadowColor: 'rgba(0,0,0,0.4)' } }, animationEasing: 'elasticOut', animationDuration: 800 } }); mixin(BoxplotSeries, seriesModelMixin, true); var BoxplotView = Chart.extend({ type: 'boxplot', getStyleUpdater: function () { return updateStyle$1; }, dispose: noop }); mixin(BoxplotView, viewMixin, true); // Update common properties var normalStyleAccessPath$1 = ['itemStyle']; var emphasisStyleAccessPath$1 = ['emphasis', 'itemStyle']; function updateStyle$1(itemGroup, data, idx) { var itemModel = data.getItemModel(idx); var normalItemStyleModel = itemModel.getModel(normalStyleAccessPath$1); var borderColor = data.getItemVisual(idx, 'color'); // Exclude borderColor. var itemStyle = normalItemStyleModel.getItemStyle(['borderColor']); var whiskerEl = itemGroup.childAt(itemGroup.whiskerIndex); whiskerEl.style.set(itemStyle); whiskerEl.style.stroke = borderColor; whiskerEl.dirty(); var bodyEl = itemGroup.childAt(itemGroup.bodyIndex); bodyEl.style.set(itemStyle); bodyEl.style.stroke = borderColor; bodyEl.dirty(); var hoverStyle = itemModel.getModel(emphasisStyleAccessPath$1).getItemStyle(); setHoverStyle(itemGroup, hoverStyle); } var borderColorQuery = ['itemStyle', 'borderColor']; var boxplotVisual = function (ecModel, api) { var globalColors = ecModel.get('color'); ecModel.eachRawSeriesByType('boxplot', function (seriesModel) { var defaulColor = globalColors[seriesModel.seriesIndex % globalColors.length]; var data = seriesModel.getData(); data.setVisual({ legendSymbol: 'roundRect', // Use name 'color' but not 'borderColor' for legend usage and // visual coding from other component like dataRange. color: seriesModel.get(borderColorQuery) || defaulColor }); // Only visible series has each data be visual encoded if (!ecModel.isSeriesFiltered(seriesModel)) { data.each(function (idx) { var itemModel = data.getItemModel(idx); data.setItemVisual( idx, {color: itemModel.get(borderColorQuery, true)} ); }); } }); }; var each$14 = each$1; var boxplotLayout = function (ecModel) { var groupResult = groupSeriesByAxis(ecModel); each$14(groupResult, function (groupItem) { var seriesModels = groupItem.seriesModels; if (!seriesModels.length) { return; } calculateBase(groupItem); each$14(seriesModels, function (seriesModel, idx) { layoutSingleSeries( seriesModel, groupItem.boxOffsetList[idx], groupItem.boxWidthList[idx] ); }); }); }; /** * Group series by axis. */ function groupSeriesByAxis(ecModel) { var result = []; var axisList = []; ecModel.eachSeriesByType('boxplot', function (seriesModel) { var baseAxis = seriesModel.getBaseAxis(); var idx = indexOf(axisList, baseAxis); if (idx < 0) { idx = axisList.length; axisList[idx] = baseAxis; result[idx] = {axis: baseAxis, seriesModels: []}; } result[idx].seriesModels.push(seriesModel); }); return result; } /** * Calculate offset and box width for each series. */ function calculateBase(groupItem) { var extent; var baseAxis = groupItem.axis; var seriesModels = groupItem.seriesModels; var seriesCount = seriesModels.length; var boxWidthList = groupItem.boxWidthList = []; var boxOffsetList = groupItem.boxOffsetList = []; var boundList = []; var bandWidth; if (baseAxis.type === 'category') { bandWidth = baseAxis.getBandWidth(); } else { var maxDataCount = 0; each$14(seriesModels, function (seriesModel) { maxDataCount = Math.max(maxDataCount, seriesModel.getData().count()); }); extent = baseAxis.getExtent(), Math.abs(extent[1] - extent[0]) / maxDataCount; } each$14(seriesModels, function (seriesModel) { var boxWidthBound = seriesModel.get('boxWidth'); if (!isArray(boxWidthBound)) { boxWidthBound = [boxWidthBound, boxWidthBound]; } boundList.push([ parsePercent$1(boxWidthBound[0], bandWidth) || 0, parsePercent$1(boxWidthBound[1], bandWidth) || 0 ]); }); var availableWidth = bandWidth * 0.8 - 2; var boxGap = availableWidth / seriesCount * 0.3; var boxWidth = (availableWidth - boxGap * (seriesCount - 1)) / seriesCount; var base = boxWidth / 2 - availableWidth / 2; each$14(seriesModels, function (seriesModel, idx) { boxOffsetList.push(base); base += boxGap + boxWidth; boxWidthList.push( Math.min(Math.max(boxWidth, boundList[idx][0]), boundList[idx][1]) ); }); } /** * Calculate points location for each series. */ function layoutSingleSeries(seriesModel, offset, boxWidth) { var coordSys = seriesModel.coordinateSystem; var data = seriesModel.getData(); var halfWidth = boxWidth / 2; var chartLayout = seriesModel.get('layout'); var variableDim = chartLayout === 'horizontal' ? 0 : 1; var constDim = 1 - variableDim; var coordDims = ['x', 'y']; var vDims = []; var cDim; each$1(data.dimensions, function (dimName) { var dimInfo = data.getDimensionInfo(dimName); var coordDim = dimInfo.coordDim; if (coordDim === coordDims[constDim]) { vDims.push(dimName); } else if (coordDim === coordDims[variableDim]) { cDim = dimName; } }); if (cDim == null || vDims.length < 5) { return; } data.each([cDim].concat(vDims), function () { var args = arguments; var axisDimVal = args[0]; var idx = args[vDims.length + 1]; var median = getPoint(args[3]); var end1 = getPoint(args[1]); var end5 = getPoint(args[5]); var whiskerEnds = [ [end1, getPoint(args[2])], [end5, getPoint(args[4])] ]; layEndLine(end1); layEndLine(end5); layEndLine(median); var bodyEnds = []; addBodyEnd(whiskerEnds[0][1], 0); addBodyEnd(whiskerEnds[1][1], 1); data.setItemLayout(idx, { chartLayout: chartLayout, initBaseline: median[constDim], median: median, bodyEnds: bodyEnds, whiskerEnds: whiskerEnds }); function getPoint(val) { var p = []; p[variableDim] = axisDimVal; p[constDim] = val; var point; if (isNaN(axisDimVal) || isNaN(val)) { point = [NaN, NaN]; } else { point = coordSys.dataToPoint(p); point[variableDim] += offset; } return point; } function addBodyEnd(point, start) { var point1 = point.slice(); var point2 = point.slice(); point1[variableDim] += halfWidth; point2[variableDim] -= halfWidth; start ? bodyEnds.push(point1, point2) : bodyEnds.push(point2, point1); } function layEndLine(endCenter) { var line = [endCenter.slice(), endCenter.slice()]; line[0][variableDim] -= halfWidth; line[1][variableDim] += halfWidth; whiskerEnds.push(line); } }); } registerVisual(boxplotVisual); registerLayout(boxplotLayout); var CandlestickSeries = SeriesModel.extend({ type: 'series.candlestick', dependencies: ['xAxis', 'yAxis', 'grid'], /** * @readOnly */ defaultValueDimensions: ['open', 'close', 'lowest', 'highest'], /** * @type {Array.<string>} * @readOnly */ dimensions: null, /** * @override */ defaultOption: { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 coordinateSystem: 'cartesian2d', legendHoverLink: true, hoverAnimation: true, // xAxisIndex: 0, // yAxisIndex: 0, layout: null, // 'horizontal' or 'vertical' itemStyle: { color: '#c23531', // 阳线 positive color0: '#314656', // 阴线 negative '#c23531', '#314656' borderWidth: 1, // FIXME // ec2中使用的是lineStyle.color 和 lineStyle.color0 borderColor: '#c23531', borderColor0: '#314656' }, emphasis: { itemStyle: { borderWidth: 2 } }, barMaxWidth: null, barMinWidth: null, barWidth: null, animationUpdate: false, animationEasing: 'linear', animationDuration: 300 }, /** * Get dimension for shadow in dataZoom * @return {string} dimension name */ getShadowDim: function () { return 'open'; }, brushSelector: function (dataIndex, data, selectors) { var itemLayout = data.getItemLayout(dataIndex); return selectors.rect(itemLayout.brushRect); } }); mixin(CandlestickSeries, seriesModelMixin, true); var CandlestickView = Chart.extend({ type: 'candlestick', getStyleUpdater: function () { return updateStyle$2; }, dispose: noop }); mixin(CandlestickView, viewMixin, true); // Update common properties var normalStyleAccessPath$2 = ['itemStyle']; var emphasisStyleAccessPath$2 = ['emphasis', 'itemStyle']; function updateStyle$2(itemGroup, data, idx) { var itemModel = data.getItemModel(idx); var normalItemStyleModel = itemModel.getModel(normalStyleAccessPath$2); var color = data.getItemVisual(idx, 'color'); var borderColor = data.getItemVisual(idx, 'borderColor') || color; // Color must be excluded. // Because symbol provide setColor individually to set fill and stroke var itemStyle = normalItemStyleModel.getItemStyle( ['color', 'color0', 'borderColor', 'borderColor0'] ); var whiskerEl = itemGroup.childAt(itemGroup.whiskerIndex); whiskerEl.useStyle(itemStyle); whiskerEl.style.stroke = borderColor; var bodyEl = itemGroup.childAt(itemGroup.bodyIndex); bodyEl.useStyle(itemStyle); bodyEl.style.fill = color; bodyEl.style.stroke = borderColor; var hoverStyle = itemModel.getModel(emphasisStyleAccessPath$2).getItemStyle(); setHoverStyle(itemGroup, hoverStyle); } var preprocessor = function (option) { if (!option || !isArray(option.series)) { return; } // Translate 'k' to 'candlestick'. each$1(option.series, function (seriesItem) { if (isObject$1(seriesItem) && seriesItem.type === 'k') { seriesItem.type = 'candlestick'; } }); }; var positiveBorderColorQuery = ['itemStyle', 'borderColor']; var negativeBorderColorQuery = ['itemStyle', 'borderColor0']; var positiveColorQuery = ['itemStyle', 'color']; var negativeColorQuery = ['itemStyle', 'color0']; var candlestickVisual = function (ecModel, api) { ecModel.eachRawSeriesByType('candlestick', function (seriesModel) { var data = seriesModel.getData(); data.setVisual({ legendSymbol: 'roundRect' }); // Only visible series has each data be visual encoded if (!ecModel.isSeriesFiltered(seriesModel)) { data.each(function (idx) { var itemModel = data.getItemModel(idx); var sign = data.getItemLayout(idx).sign; data.setItemVisual( idx, { color: itemModel.get( sign > 0 ? positiveColorQuery : negativeColorQuery ), borderColor: itemModel.get( sign > 0 ? positiveBorderColorQuery : negativeBorderColorQuery ) } ); }); } }); }; var retrieve2$1 = retrieve2; var candlestickLayout = function (ecModel) { ecModel.eachSeriesByType('candlestick', function (seriesModel) { var coordSys = seriesModel.coordinateSystem; var data = seriesModel.getData(); var candleWidth = calculateCandleWidth(seriesModel, data); var chartLayout = seriesModel.get('layout'); var variableDim = chartLayout === 'horizontal' ? 0 : 1; var constDim = 1 - variableDim; var coordDims = ['x', 'y']; var vDims = []; var cDim; each$1(data.dimensions, function (dimName) { var dimInfo = data.getDimensionInfo(dimName); var coordDim = dimInfo.coordDim; if (coordDim === coordDims[constDim]) { vDims.push(dimName); } else if (coordDim === coordDims[variableDim]) { cDim = dimName; } }); if (cDim == null || vDims.length < 4) { return; } var dataIndex = 0; data.each([cDim].concat(vDims), function () { var args = arguments; var axisDimVal = args[0]; var idx = args[vDims.length + 1]; var openVal = args[1]; var closeVal = args[2]; var lowestVal = args[3]; var highestVal = args[4]; var ocLow = Math.min(openVal, closeVal); var ocHigh = Math.max(openVal, closeVal); var ocLowPoint = getPoint(ocLow); var ocHighPoint = getPoint(ocHigh); var lowestPoint = getPoint(lowestVal); var highestPoint = getPoint(highestVal); var whiskerEnds = [ [ subPixelOptimizePoint(highestPoint), subPixelOptimizePoint(ocHighPoint) ], [ subPixelOptimizePoint(lowestPoint), subPixelOptimizePoint(ocLowPoint) ] ]; var bodyEnds = []; addBodyEnd(ocHighPoint, 0); addBodyEnd(ocLowPoint, 1); var sign; if (openVal > closeVal) { sign = -1; } else if (openVal < closeVal) { sign = 1; } else { // If close === open, compare with close of last record if (dataIndex > 0) { sign = data.getItemModel(dataIndex - 1).get()[2] <= closeVal ? 1 : -1; } else { // No record of previous, set to be positive sign = 1; } } data.setItemLayout(idx, { chartLayout: chartLayout, sign: sign, initBaseline: openVal > closeVal ? ocHighPoint[constDim] : ocLowPoint[constDim], // open point. bodyEnds: bodyEnds, whiskerEnds: whiskerEnds, brushRect: makeBrushRect() }); ++dataIndex; function getPoint(val) { var p = []; p[variableDim] = axisDimVal; p[constDim] = val; return (isNaN(axisDimVal) || isNaN(val)) ? [NaN, NaN] : coordSys.dataToPoint(p); } function addBodyEnd(point, start) { var point1 = point.slice(); var point2 = point.slice(); point1[variableDim] = subPixelOptimize( point1[variableDim] + candleWidth / 2, 1, false ); point2[variableDim] = subPixelOptimize( point2[variableDim] - candleWidth / 2, 1, true ); start ? bodyEnds.push(point1, point2) : bodyEnds.push(point2, point1); } function makeBrushRect() { var pmin = getPoint(Math.min(openVal, closeVal, lowestVal, highestVal)); var pmax = getPoint(Math.max(openVal, closeVal, lowestVal, highestVal)); pmin[variableDim] -= candleWidth / 2; pmax[variableDim] -= candleWidth / 2; return { x: pmin[0], y: pmin[1], width: constDim ? candleWidth : pmax[0] - pmin[0], height: constDim ? pmax[1] - pmin[1] : candleWidth }; } function subPixelOptimizePoint(point) { point[variableDim] = subPixelOptimize(point[variableDim], 1); return point; } }); }); }; function calculateCandleWidth(seriesModel, data) { var baseAxis = seriesModel.getBaseAxis(); var extent; var bandWidth = baseAxis.type === 'category' ? baseAxis.getBandWidth() : ( extent = baseAxis.getExtent(), Math.abs(extent[1] - extent[0]) / data.count() ); var barMaxWidth = parsePercent$1( retrieve2$1(seriesModel.get('barMaxWidth'), bandWidth), bandWidth ); var barMinWidth = parsePercent$1( retrieve2$1(seriesModel.get('barMinWidth'), 1), bandWidth ); var barWidth = seriesModel.get('barWidth'); return barWidth != null ? parsePercent$1(barWidth, bandWidth) // Put max outer to ensure bar visible in spite of overlap. : Math.max(Math.min(bandWidth / 2, barMaxWidth), barMinWidth); } registerPreprocessor(preprocessor); registerVisual(candlestickVisual); registerLayout(candlestickLayout); SeriesModel.extend({ type: 'series.effectScatter', dependencies: ['grid', 'polar'], getInitialData: function (option, ecModel) { return createListFromArray(this.getSource(), this); }, brushSelector: 'point', defaultOption: { coordinateSystem: 'cartesian2d', zlevel: 0, z: 2, legendHoverLink: true, effectType: 'ripple', progressive: 0, // When to show the effect, option: 'render'|'emphasis' showEffectOn: 'render', // Ripple effect config rippleEffect: { period: 4, // Scale of ripple scale: 2.5, // Brush type can be fill or stroke brushType: 'fill' }, // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, // Polar coordinate system // polarIndex: 0, // Geo coordinate system // geoIndex: 0, // symbol: null, // 图形类型 symbolSize: 10 // 图形大小,半宽(半径)参数,当图形为方向或菱形则总宽度为symbolSize * 2 // symbolRotate: null, // 图形旋转控制 // large: false, // Available when large is true // largeThreshold: 2000, // itemStyle: { // opacity: 1 // } } }); /** * Symbol with ripple effect * @module echarts/chart/helper/EffectSymbol */ var EFFECT_RIPPLE_NUMBER = 3; function normalizeSymbolSize$1(symbolSize) { if (!isArray(symbolSize)) { symbolSize = [+symbolSize, +symbolSize]; } return symbolSize; } function updateRipplePath(rippleGroup, effectCfg) { rippleGroup.eachChild(function (ripplePath) { ripplePath.attr({ z: effectCfg.z, zlevel: effectCfg.zlevel, style: { stroke: effectCfg.brushType === 'stroke' ? effectCfg.color : null, fill: effectCfg.brushType === 'fill' ? effectCfg.color : null } }); }); } /** * @constructor * @param {module:echarts/data/List} data * @param {number} idx * @extends {module:zrender/graphic/Group} */ function EffectSymbol(data, idx) { Group.call(this); var symbol = new SymbolClz$1(data, idx); var rippleGroup = new Group(); this.add(symbol); this.add(rippleGroup); rippleGroup.beforeUpdate = function () { this.attr(symbol.getScale()); }; this.updateData(data, idx); } var effectSymbolProto = EffectSymbol.prototype; effectSymbolProto.stopEffectAnimation = function () { this.childAt(1).removeAll(); }; effectSymbolProto.startEffectAnimation = function (effectCfg) { var symbolType = effectCfg.symbolType; var color = effectCfg.color; var rippleGroup = this.childAt(1); for (var i = 0; i < EFFECT_RIPPLE_NUMBER; i++) { // var ripplePath = createSymbol( // symbolType, -0.5, -0.5, 1, 1, color // ); // If width/height are set too small (e.g., set to 1) on ios10 // and macOS Sierra, a circle stroke become a rect, no matter what // the scale is set. So we set width/height as 2. See #4136. var ripplePath = createSymbol( symbolType, -1, -1, 2, 2, color ); ripplePath.attr({ style: { strokeNoScale: true }, z2: 99, silent: true, scale: [0.5, 0.5] }); var delay = -i / EFFECT_RIPPLE_NUMBER * effectCfg.period + effectCfg.effectOffset; // TODO Configurable effectCfg.period ripplePath.animate('', true) .when(effectCfg.period, { scale: [effectCfg.rippleScale / 2, effectCfg.rippleScale / 2] }) .delay(delay) .start(); ripplePath.animateStyle(true) .when(effectCfg.period, { opacity: 0 }) .delay(delay) .start(); rippleGroup.add(ripplePath); } updateRipplePath(rippleGroup, effectCfg); }; /** * Update effect symbol */ effectSymbolProto.updateEffectAnimation = function (effectCfg) { var oldEffectCfg = this._effectCfg; var rippleGroup = this.childAt(1); // Must reinitialize effect if following configuration changed var DIFFICULT_PROPS = ['symbolType', 'period', 'rippleScale']; for (var i = 0; i < DIFFICULT_PROPS.length; i++) { var propName = DIFFICULT_PROPS[i]; if (oldEffectCfg[propName] !== effectCfg[propName]) { this.stopEffectAnimation(); this.startEffectAnimation(effectCfg); return; } } updateRipplePath(rippleGroup, effectCfg); }; /** * Highlight symbol */ effectSymbolProto.highlight = function () { this.trigger('emphasis'); }; /** * Downplay symbol */ effectSymbolProto.downplay = function () { this.trigger('normal'); }; /** * Update symbol properties * @param {module:echarts/data/List} data * @param {number} idx */ effectSymbolProto.updateData = function (data, idx) { var seriesModel = data.hostModel; this.childAt(0).updateData(data, idx); var rippleGroup = this.childAt(1); var itemModel = data.getItemModel(idx); var symbolType = data.getItemVisual(idx, 'symbol'); var symbolSize = normalizeSymbolSize$1(data.getItemVisual(idx, 'symbolSize')); var color = data.getItemVisual(idx, 'color'); rippleGroup.attr('scale', symbolSize); rippleGroup.traverse(function (ripplePath) { ripplePath.attr({ fill: color }); }); var symbolOffset = itemModel.getShallow('symbolOffset'); if (symbolOffset) { var pos = rippleGroup.position; pos[0] = parsePercent$1(symbolOffset[0], symbolSize[0]); pos[1] = parsePercent$1(symbolOffset[1], symbolSize[1]); } rippleGroup.rotation = (itemModel.getShallow('symbolRotate') || 0) * Math.PI / 180 || 0; var effectCfg = {}; effectCfg.showEffectOn = seriesModel.get('showEffectOn'); effectCfg.rippleScale = itemModel.get('rippleEffect.scale'); effectCfg.brushType = itemModel.get('rippleEffect.brushType'); effectCfg.period = itemModel.get('rippleEffect.period') * 1000; effectCfg.effectOffset = idx / data.count(); effectCfg.z = itemModel.getShallow('z') || 0; effectCfg.zlevel = itemModel.getShallow('zlevel') || 0; effectCfg.symbolType = symbolType; effectCfg.color = color; this.off('mouseover').off('mouseout').off('emphasis').off('normal'); if (effectCfg.showEffectOn === 'render') { this._effectCfg ? this.updateEffectAnimation(effectCfg) : this.startEffectAnimation(effectCfg); this._effectCfg = effectCfg; } else { // Not keep old effect config this._effectCfg = null; this.stopEffectAnimation(); var symbol = this.childAt(0); var onEmphasis = function () { symbol.highlight(); if (effectCfg.showEffectOn !== 'render') { this.startEffectAnimation(effectCfg); } }; var onNormal = function () { symbol.downplay(); if (effectCfg.showEffectOn !== 'render') { this.stopEffectAnimation(); } }; this.on('mouseover', onEmphasis, this) .on('mouseout', onNormal, this) .on('emphasis', onEmphasis, this) .on('normal', onNormal, this); } this._effectCfg = effectCfg; }; effectSymbolProto.fadeOut = function (cb) { this.off('mouseover').off('mouseout').off('emphasis').off('normal'); cb && cb(); }; inherits(EffectSymbol, Group); extendChartView({ type: 'effectScatter', init: function () { this._symbolDraw = new SymbolDraw(EffectSymbol); }, render: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var effectSymbolDraw = this._symbolDraw; effectSymbolDraw.updateData(data); this.group.add(effectSymbolDraw.group); }, updateTransform: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); this.group.dirty(); var res = pointsLayout().reset(seriesModel); if (res.progress) { res.progress({ start: 0, end: data.count() }, data); } this._symbolDraw.updateLayout(data); }, _updateGroupTransform: function (seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.getRoamTransform) { this.group.transform = clone$2(coordSys.getRoamTransform()); this.group.decomposeTransform(); } }, remove: function (ecModel, api) { this._symbolDraw && this._symbolDraw.remove(api); }, dispose: function () {} }); registerVisual(visualSymbol('effectScatter', 'circle')); registerLayout(pointsLayout('effectScatter')); var globalObj$1 = typeof window === 'undefined' ? global : window; var Uint32Arr = globalObj$1.Uint32Array || Array; var Float64Arr = globalObj$1.Float64Array || Array; function compatEc2(seriesOpt) { var data = seriesOpt.data; if (data && data[0] && data[0][0] && data[0][0].coord) { if (__DEV__) { console.warn('Lines data configuration has been changed to' + ' { coords:[[1,2],[2,3]] }'); } seriesOpt.data = map(data, function (itemOpt) { var coords = [ itemOpt[0].coord, itemOpt[1].coord ]; var target = { coords: coords }; if (itemOpt[0].name) { target.fromName = itemOpt[0].name; } if (itemOpt[1].name) { target.toName = itemOpt[1].name; } return mergeAll([target, itemOpt[0], itemOpt[1]]); }); } } var LinesSeries = SeriesModel.extend({ type: 'series.lines', dependencies: ['grid', 'polar'], visualColorAccessPath: 'lineStyle.color', init: function (option) { // Not using preprocessor because mergeOption may not have series.type compatEc2(option); var result = this._processFlatCoordsArray(option.data); this._flatCoords = result.flatCoords; this._flatCoordsOffset = result.flatCoordsOffset; if (result.flatCoords) { option.data = new Float32Array(result.count); } LinesSeries.superApply(this, 'init', arguments); }, mergeOption: function (option) { compatEc2(option); if (option.data) { // Only update when have option data to merge. var result = this._processFlatCoordsArray(option.data); this._flatCoords = result.flatCoords; this._flatCoordsOffset = result.flatCoordsOffset; if (result.flatCoords) { option.data = new Float32Array(result.count); } } LinesSeries.superApply(this, 'mergeOption', arguments); }, appendData: function (params) { var result = this._processFlatCoordsArray(params.data); if (result.flatCoords) { if (!this._flatCoords) { this._flatCoords = result.flatCoords; this._flatCoordsOffset = result.flatCoordsOffset; } else { this._flatCoords = concatArray(this._flatCoords, result.flatCoords); this._flatCoordsOffset = concatArray(this._flatCoordsOffset, result.flatCoordsOffset); } params.data = new Float32Array(result.count); } this.getRawData().appendData(params.data); }, _getCoordsFromItemModel: function (idx) { var itemModel = this.getData().getItemModel(idx); var coords = (itemModel.option instanceof Array) ? itemModel.option : itemModel.getShallow('coords'); if (__DEV__) { if (!(coords instanceof Array && coords.length > 0 && coords[0] instanceof Array)) { throw new Error('Invalid coords ' + JSON.stringify(coords) + '. Lines must have 2d coords array in data item.'); } } return coords; }, getLineCoordsCount: function (idx) { if (this._flatCoordsOffset) { return this._flatCoordsOffset[idx * 2 + 1]; } else { return this._getCoordsFromItemModel(idx).length; } }, getLineCoords: function (idx, out) { if (this._flatCoordsOffset) { var offset = this._flatCoordsOffset[idx * 2]; var len = this._flatCoordsOffset[idx * 2 + 1]; for (var i = 0; i < len; i++) { out[i] = out[i] || []; out[i][0] = this._flatCoords[offset + i * 2]; out[i][1] = this._flatCoords[offset + i * 2 + 1]; } return len; } else { var coords = this._getCoordsFromItemModel(idx); for (var i = 0; i < coords.length; i++) { out[i] = out[i] || []; out[i][0] = coords[i][0]; out[i][1] = coords[i][1]; } return coords.length; } }, _processFlatCoordsArray: function (data) { var startOffset = 0; if (this._flatCoords) { startOffset = this._flatCoords.length; } // Stored as a typed array. In format // Points Count(2) | x | y | x | y | Points Count(3) | x | y | x | y | x | y | if (typeof data[0] === 'number') { var len = data.length; // Store offset and len of each segment var coordsOffsetAndLenStorage = new Uint32Arr(len); var coordsStorage = new Float64Arr(len); var coordsCursor = 0; var offsetCursor = 0; var dataCount = 0; for (var i = 0; i < len;) { dataCount++; var count = data[i++]; // Offset coordsOffsetAndLenStorage[offsetCursor++] = coordsCursor + startOffset; // Len coordsOffsetAndLenStorage[offsetCursor++] = count; for (var k = 0; k < count; k++) { var x = data[i++]; var y = data[i++]; coordsStorage[coordsCursor++] = x; coordsStorage[coordsCursor++] = y; if (i > len) { if (__DEV__) { throw new Error('Invalid data format.'); } } } } return { flatCoordsOffset: new Uint32Array(coordsOffsetAndLenStorage.buffer, 0, offsetCursor), flatCoords: coordsStorage, count: dataCount }; } return { flatCoordsOffset: null, flatCoords: null, count: data.length }; }, getInitialData: function (option, ecModel) { if (__DEV__) { var CoordSys = CoordinateSystemManager.get(option.coordinateSystem); if (!CoordSys) { throw new Error('Unkown coordinate system ' + option.coordinateSystem); } } var lineData = new List(['value'], this); lineData.hasItemOption = false; lineData.initData(option.data, [], function (dataItem, dimName, dataIndex, dimIndex) { // dataItem is simply coords if (dataItem instanceof Array) { return NaN; } else { lineData.hasItemOption = true; var value = dataItem.value; if (value != null) { return value instanceof Array ? value[dimIndex] : value; } } }); return lineData; }, formatTooltip: function (dataIndex) { var data = this.getData(); var itemModel = data.getItemModel(dataIndex); var name = itemModel.get('name'); if (name) { return name; } var fromName = itemModel.get('fromName'); var toName = itemModel.get('toName'); var html = []; fromName != null && html.push(fromName); toName != null && html.push(toName); return encodeHTML(html.join(' > ')); }, preventIncremental: function () { return !!this.get('effect.show'); }, getProgressive: function () { var progressive = this.option.progressive; if (progressive == null) { return this.option.large ? 1e4 : this.get('progressive'); } return progressive; }, getProgressiveThreshold: function () { var progressiveThreshold = this.option.progressiveThreshold; if (progressiveThreshold == null) { return this.option.large ? 2e4 : this.get('progressiveThreshold'); } return progressiveThreshold; }, defaultOption: { coordinateSystem: 'geo', zlevel: 0, z: 2, legendHoverLink: true, hoverAnimation: true, // Cartesian coordinate system xAxisIndex: 0, yAxisIndex: 0, symbol: ['none', 'none'], symbolSize: [10, 10], // Geo coordinate system geoIndex: 0, effect: { show: false, period: 4, // Animation delay. support callback // delay: 0, // If move with constant speed px/sec // period will be ignored if this property is > 0, constantSpeed: 0, symbol: 'circle', symbolSize: 3, loop: true, // Length of trail, 0 - 1 trailLength: 0.2 // Same with lineStyle.color // color }, large: false, // Available when large is true largeThreshold: 2000, // If lines are polyline // polyline not support curveness, label, animation polyline: false, label: { show: false, position: 'end' // distance: 5, // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 }, lineStyle: { opacity: 0.5 } } }); /** * Provide effect for line * @module echarts/chart/helper/EffectLine */ /** * @constructor * @extends {module:zrender/graphic/Group} * @alias {module:echarts/chart/helper/Line} */ function EffectLine(lineData, idx, seriesScope) { Group.call(this); this.add(this.createLine(lineData, idx, seriesScope)); this._updateEffectSymbol(lineData, idx); } var effectLineProto = EffectLine.prototype; effectLineProto.createLine = function (lineData, idx, seriesScope) { return new Line$1(lineData, idx, seriesScope); }; effectLineProto._updateEffectSymbol = function (lineData, idx) { var itemModel = lineData.getItemModel(idx); var effectModel = itemModel.getModel('effect'); var size = effectModel.get('symbolSize'); var symbolType = effectModel.get('symbol'); if (!isArray(size)) { size = [size, size]; } var color = effectModel.get('color') || lineData.getItemVisual(idx, 'color'); var symbol = this.childAt(1); if (this._symbolType !== symbolType) { // Remove previous this.remove(symbol); symbol = createSymbol( symbolType, -0.5, -0.5, 1, 1, color ); symbol.z2 = 100; symbol.culling = true; this.add(symbol); } // Symbol may be removed if loop is false if (!symbol) { return; } // Shadow color is same with color in default symbol.setStyle('shadowColor', color); symbol.setStyle(effectModel.getItemStyle(['color'])); symbol.attr('scale', size); symbol.setColor(color); symbol.attr('scale', size); this._symbolType = symbolType; this._updateEffectAnimation(lineData, effectModel, idx); }; effectLineProto._updateEffectAnimation = function (lineData, effectModel, idx) { var symbol = this.childAt(1); if (!symbol) { return; } var self = this; var points = lineData.getItemLayout(idx); var period = effectModel.get('period') * 1000; var loop = effectModel.get('loop'); var constantSpeed = effectModel.get('constantSpeed'); var delayExpr = retrieve(effectModel.get('delay'), function (idx) { return idx / lineData.count() * period / 3; }); var isDelayFunc = typeof delayExpr === 'function'; // Ignore when updating symbol.ignore = true; this.updateAnimationPoints(symbol, points); if (constantSpeed > 0) { period = this.getLineLength(symbol) / constantSpeed * 1000; } if (period !== this._period || loop !== this._loop) { symbol.stopAnimation(); var delay = delayExpr; if (isDelayFunc) { delay = delayExpr(idx); } if (symbol.__t > 0) { delay = -period * symbol.__t; } symbol.__t = 0; var animator = symbol.animate('', loop) .when(period, { __t: 1 }) .delay(delay) .during(function () { self.updateSymbolPosition(symbol); }); if (!loop) { animator.done(function () { self.remove(symbol); }); } animator.start(); } this._period = period; this._loop = loop; }; effectLineProto.getLineLength = function (symbol) { // Not so accurate return (dist(symbol.__p1, symbol.__cp1) + dist(symbol.__cp1, symbol.__p2)); }; effectLineProto.updateAnimationPoints = function (symbol, points) { symbol.__p1 = points[0]; symbol.__p2 = points[1]; symbol.__cp1 = points[2] || [ (points[0][0] + points[1][0]) / 2, (points[0][1] + points[1][1]) / 2 ]; }; effectLineProto.updateData = function (lineData, idx, seriesScope) { this.childAt(0).updateData(lineData, idx, seriesScope); this._updateEffectSymbol(lineData, idx); }; effectLineProto.updateSymbolPosition = function (symbol) { var p1 = symbol.__p1; var p2 = symbol.__p2; var cp1 = symbol.__cp1; var t = symbol.__t; var pos = symbol.position; var quadraticAt$$1 = quadraticAt; var quadraticDerivativeAt$$1 = quadraticDerivativeAt; pos[0] = quadraticAt$$1(p1[0], cp1[0], p2[0], t); pos[1] = quadraticAt$$1(p1[1], cp1[1], p2[1], t); // Tangent var tx = quadraticDerivativeAt$$1(p1[0], cp1[0], p2[0], t); var ty = quadraticDerivativeAt$$1(p1[1], cp1[1], p2[1], t); symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; symbol.ignore = false; }; effectLineProto.updateLayout = function (lineData, idx) { this.childAt(0).updateLayout(lineData, idx); var effectModel = lineData.getItemModel(idx).getModel('effect'); this._updateEffectAnimation(lineData, effectModel, idx); }; inherits(EffectLine, Group); /** * @module echarts/chart/helper/Line */ /** * @constructor * @extends {module:zrender/graphic/Group} * @alias {module:echarts/chart/helper/Polyline} */ function Polyline$2(lineData, idx, seriesScope) { Group.call(this); this._createPolyline(lineData, idx, seriesScope); } var polylineProto = Polyline$2.prototype; polylineProto._createPolyline = function (lineData, idx, seriesScope) { // var seriesModel = lineData.hostModel; var points = lineData.getItemLayout(idx); var line = new Polyline({ shape: { points: points } }); this.add(line); this._updateCommonStl(lineData, idx, seriesScope); }; polylineProto.updateData = function (lineData, idx, seriesScope) { var seriesModel = lineData.hostModel; var line = this.childAt(0); var target = { shape: { points: lineData.getItemLayout(idx) } }; updateProps(line, target, seriesModel, idx); this._updateCommonStl(lineData, idx, seriesScope); }; polylineProto._updateCommonStl = function (lineData, idx, seriesScope) { var line = this.childAt(0); var itemModel = lineData.getItemModel(idx); var visualColor = lineData.getItemVisual(idx, 'color'); var lineStyle = seriesScope && seriesScope.lineStyle; var hoverLineStyle = seriesScope && seriesScope.hoverLineStyle; if (!seriesScope || lineData.hasItemOption) { lineStyle = itemModel.getModel('lineStyle').getLineStyle(); hoverLineStyle = itemModel.getModel('emphasis.lineStyle').getLineStyle(); } line.useStyle(defaults( { strokeNoScale: true, fill: 'none', stroke: visualColor }, lineStyle )); line.hoverStyle = hoverLineStyle; setHoverStyle(this); }; polylineProto.updateLayout = function (lineData, idx) { var polyline = this.childAt(0); polyline.setShape('points', lineData.getItemLayout(idx)); }; inherits(Polyline$2, Group); /** * Provide effect for line * @module echarts/chart/helper/EffectLine */ /** * @constructor * @extends {module:echarts/chart/helper/EffectLine} * @alias {module:echarts/chart/helper/Polyline} */ function EffectPolyline(lineData, idx, seriesScope) { EffectLine.call(this, lineData, idx, seriesScope); this._lastFrame = 0; this._lastFramePercent = 0; } var effectPolylineProto = EffectPolyline.prototype; // Overwrite effectPolylineProto.createLine = function (lineData, idx, seriesScope) { return new Polyline$2(lineData, idx, seriesScope); }; // Overwrite effectPolylineProto.updateAnimationPoints = function (symbol, points) { this._points = points; var accLenArr = [0]; var len$$1 = 0; for (var i = 1; i < points.length; i++) { var p1 = points[i - 1]; var p2 = points[i]; len$$1 += dist(p1, p2); accLenArr.push(len$$1); } if (len$$1 === 0) { return; } for (var i = 0; i < accLenArr.length; i++) { accLenArr[i] /= len$$1; } this._offsets = accLenArr; this._length = len$$1; }; // Overwrite effectPolylineProto.getLineLength = function (symbol) { return this._length; }; // Overwrite effectPolylineProto.updateSymbolPosition = function (symbol) { var t = symbol.__t; var points = this._points; var offsets = this._offsets; var len$$1 = points.length; if (!offsets) { // Has length 0 return; } var lastFrame = this._lastFrame; var frame; if (t < this._lastFramePercent) { // Start from the next frame // PENDING start from lastFrame ? var start = Math.min(lastFrame + 1, len$$1 - 1); for (frame = start; frame >= 0; frame--) { if (offsets[frame] <= t) { break; } } // PENDING really need to do this ? frame = Math.min(frame, len$$1 - 2); } else { for (var frame = lastFrame; frame < len$$1; frame++) { if (offsets[frame] > t) { break; } } frame = Math.min(frame - 1, len$$1 - 2); } lerp( symbol.position, points[frame], points[frame + 1], (t - offsets[frame]) / (offsets[frame + 1] - offsets[frame]) ); var tx = points[frame + 1][0] - points[frame][0]; var ty = points[frame + 1][1] - points[frame][1]; symbol.rotation = -Math.atan2(ty, tx) - Math.PI / 2; this._lastFrame = frame; this._lastFramePercent = t; symbol.ignore = false; }; inherits(EffectPolyline, EffectLine); // TODO Batch by color var LargeLineShape = extendShape({ shape: { polyline: false, curveness: 0, segs: [] }, buildPath: function (path, shape) { var segs = shape.segs; var curveness = shape.curveness; if (shape.polyline) { for (var i = 0; i < segs.length;) { var count = segs[i++]; if (count > 0) { path.moveTo(segs[i++], segs[i++]); for (var k = 1; k < count; k++) { path.lineTo(segs[i++], segs[i++]); } } } } else { for (var i = 0; i < segs.length;) { var x0 = segs[i++]; var y0 = segs[i++]; var x1 = segs[i++]; var y1 = segs[i++]; path.moveTo(x0, y0); if (curveness > 0) { var x2 = (x0 + x1) / 2 - (y0 - y1) * curveness; var y2 = (y0 + y1) / 2 - (x1 - x0) * curveness; path.quadraticCurveTo(x2, y2, x1, y1); } else { path.lineTo(x1, y1); } } } }, findDataIndex: function (x, y) { var shape = this.shape; var segs = shape.segs; var curveness = shape.curveness; if (shape.polyline) { var dataIndex = 0; for (var i = 0; i < segs.length;) { var count = segs[i++]; if (count > 0) { var x0 = segs[i++]; var y0 = segs[i++]; for (var k = 1; k < count; k++) { var x1 = segs[i++]; var y1 = segs[i++]; if (containStroke$1(x0, y0, x1, y1)) { return dataIndex; } } } dataIndex++; } } else { var dataIndex = 0; for (var i = 0; i < segs.length;) { var x0 = segs[i++]; var y0 = segs[i++]; var x1 = segs[i++]; var y1 = segs[i++]; if (curveness > 0) { var x2 = (x0 + x1) / 2 - (y0 - y1) * curveness; var y2 = (y0 + y1) / 2 - (x1 - x0) * curveness; if (containStroke$3(x0, y0, x2, y2, x1, y1)) { return dataIndex; } } else { if (containStroke$1(x0, y0, x1, y1)) { return dataIndex; } } dataIndex++; } } return -1; } }); function LargeLineDraw() { this.group = new Group(); } var largeLineProto = LargeLineDraw.prototype; largeLineProto.isPersistent = function () { return !this._incremental; }; /** * Update symbols draw by new data * @param {module:echarts/data/List} data */ largeLineProto.updateData = function (data) { this.group.removeAll(); var lineEl = new LargeLineShape({ rectHover: true, cursor: 'default' }); lineEl.setShape({ segs: data.getLayout('linesPoints') }); this._setCommon(lineEl, data); // Add back this.group.add(lineEl); this._incremental = null; }; /** * @override */ largeLineProto.incrementalPrepareUpdate = function (data) { this.group.removeAll(); this._clearIncremental(); if (data.count() > 5e5) { if (!this._incremental) { this._incremental = new IncrementalDisplayble({ silent: true }); } this.group.add(this._incremental); } else { this._incremental = null; } }; /** * @override */ largeLineProto.incrementalUpdate = function (taskParams, data) { var lineEl = new LargeLineShape(); lineEl.setShape({ segs: data.getLayout('linesPoints') }); this._setCommon(lineEl, data, !!this._incremental); if (!this._incremental) { lineEl.rectHover = true; lineEl.cursor = 'default'; lineEl.__startIndex = taskParams.start; this.group.add(lineEl); } else { this._incremental.addDisplayable(lineEl, true); } }; /** * @override */ largeLineProto.remove = function () { this._clearIncremental(); this._incremental = null; this.group.removeAll(); }; largeLineProto._setCommon = function (lineEl, data, isIncremental) { var hostModel = data.hostModel; lineEl.setShape({ polyline: hostModel.get('polyline'), curveness: hostModel.get('lineStyle.curveness') }); lineEl.useStyle( hostModel.getModel('lineStyle').getLineStyle() ); lineEl.style.strokeNoScale = true; var visualColor = data.getVisual('color'); if (visualColor) { lineEl.setStyle('stroke', visualColor); } lineEl.setStyle('fill'); if (!isIncremental) { // Enable tooltip // PENDING May have performance issue when path is extremely large lineEl.seriesIndex = hostModel.seriesIndex; lineEl.on('mousemove', function (e) { lineEl.dataIndex = null; var dataIndex = lineEl.findDataIndex(e.offsetX, e.offsetY); if (dataIndex > 0) { // Provide dataIndex for tooltip lineEl.dataIndex = dataIndex + lineEl.__startIndex; } }); } }; largeLineProto._clearIncremental = function () { var incremental = this._incremental; if (incremental) { incremental.clearDisplaybles(); } }; var linesLayout = { seriesType: 'lines', plan: createRenderPlanner(), reset: function (seriesModel) { var coordSys = seriesModel.coordinateSystem; var isPolyline = seriesModel.get('polyline'); var isLarge = seriesModel.pipelineContext.large; function progress(params, lineData) { var lineCoords = []; if (isLarge) { var points; var segCount = params.end - params.start; if (isPolyline) { var totalCoordsCount = 0; for (var i = params.start; i < params.end; i++) { totalCoordsCount += seriesModel.getLineCoordsCount(i); } points = new Float32Array(segCount + totalCoordsCount * 2); } else { points = new Float32Array(segCount * 2); } var offset = 0; var pt = []; for (var i = params.start; i < params.end; i++) { var len = seriesModel.getLineCoords(i, lineCoords); if (isPolyline) { points[offset++] = len; } for (var k = 0; k < len; k++) { pt = coordSys.dataToPoint(lineCoords[k], false, pt); points[offset++] = pt[0]; points[offset++] = pt[1]; } } lineData.setLayout('linesPoints', points); } else { for (var i = params.start; i < params.end; i++) { var itemModel = lineData.getItemModel(i); var len = seriesModel.getLineCoords(i, lineCoords); var pts = []; if (isPolyline) { for (var j = 0; j < len; j++) { pts.push(coordSys.dataToPoint(lineCoords[j])); } } else { pts[0] = coordSys.dataToPoint(lineCoords[0]); pts[1] = coordSys.dataToPoint(lineCoords[1]); var curveness = itemModel.get('lineStyle.curveness'); if (+curveness) { pts[2] = [ (pts[0][0] + pts[1][0]) / 2 - (pts[0][1] - pts[1][1]) * curveness, (pts[0][1] + pts[1][1]) / 2 - (pts[1][0] - pts[0][0]) * curveness ]; } } lineData.setItemLayout(i, pts); } } } return { progress: progress }; } }; extendChartView({ type: 'lines', init: function () {}, render: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var lineDraw = this._updateLineDraw(data, seriesModel); var zlevel = seriesModel.get('zlevel'); var trailLength = seriesModel.get('effect.trailLength'); var zr = api.getZr(); // Avoid the drag cause ghost shadow // FIXME Better way ? // SVG doesn't support var isSvg = zr.painter.getType() === 'svg'; if (!isSvg) { zr.painter.getLayer(zlevel).clear(true); } // Config layer with motion blur if (this._lastZlevel != null && !isSvg) { zr.configLayer(this._lastZlevel, { motionBlur: false }); } if (this._showEffect(seriesModel) && trailLength) { if (__DEV__) { var notInIndividual = false; ecModel.eachSeries(function (otherSeriesModel) { if (otherSeriesModel !== seriesModel && otherSeriesModel.get('zlevel') === zlevel) { notInIndividual = true; } }); notInIndividual && console.warn('Lines with trail effect should have an individual zlevel'); } if (!isSvg) { zr.configLayer(zlevel, { motionBlur: true, lastFrameAlpha: Math.max(Math.min(trailLength / 10 + 0.9, 1), 0) }); } } lineDraw.updateData(data); this._lastZlevel = zlevel; this._finished = true; }, incrementalPrepareRender: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var lineDraw = this._updateLineDraw(data, seriesModel); lineDraw.incrementalPrepareUpdate(data); this._clearLayer(api); this._finished = false; }, incrementalRender: function (taskParams, seriesModel, ecModel) { this._lineDraw.incrementalUpdate(taskParams, seriesModel.getData()); this._finished = taskParams.end === seriesModel.getData().count(); }, updateTransform: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); if (!this._finished || seriesModel.pipelineContext.large) { // TODO Don't have to do update in large mode. Only do it when there are millions of data. return { update: true }; } else { // TODO Use same logic with ScatterView. // Manually update layout var res = linesLayout.reset(seriesModel); if (res.progress) { res.progress({ start: 0, end: data.count() }, data); } this._lineDraw.updateLayout(); this._clearLayer(api); } }, _updateLineDraw: function (data, seriesModel) { var lineDraw = this._lineDraw; var hasEffect = this._showEffect(seriesModel); var isPolyline = !!seriesModel.get('polyline'); var pipelineContext = seriesModel.pipelineContext; var isLargeDraw = pipelineContext.large; if (__DEV__) { if (hasEffect && isLargeDraw) { console.warn('Large lines not support effect'); } } if (!lineDraw || hasEffect !== this._hasEffet || isPolyline !== this._isPolyline || isLargeDraw !== this._isLargeDraw ) { if (lineDraw) { lineDraw.remove(); } lineDraw = this._lineDraw = isLargeDraw ? new LargeLineDraw() : new LineDraw( isPolyline ? (hasEffect ? EffectPolyline : Polyline$2) : (hasEffect ? EffectLine : Line$1) ); this._hasEffet = hasEffect; this._isPolyline = isPolyline; this._isLargeDraw = isLargeDraw; this.group.removeAll(); } this.group.add(lineDraw.group); return lineDraw; }, _showEffect: function (seriesModel) { return !!seriesModel.get('effect.show'); }, _clearLayer: function (api) { // Not use motion when dragging or zooming var zr = api.getZr(); var isSvg = zr.painter.getType() === 'svg'; if (!isSvg && this._lastZlevel != null) { zr.painter.getLayer(this._lastZlevel).clear(true); } }, remove: function (ecModel, api) { this._lineDraw && this._lineDraw.remove(); this._lineDraw = null; // Clear motion when lineDraw is removed this._clearLayer(api); }, dispose: function () {} }); function normalize$2(a) { if (!(a instanceof Array)) { a = [a, a]; } return a; } var opacityQuery = 'lineStyle.opacity'.split('.'); var linesVisual = { seriesType: 'lines', reset: function (seriesModel, ecModel, api) { var symbolType = normalize$2(seriesModel.get('symbol')); var symbolSize = normalize$2(seriesModel.get('symbolSize')); var data = seriesModel.getData(); data.setVisual('fromSymbol', symbolType && symbolType[0]); data.setVisual('toSymbol', symbolType && symbolType[1]); data.setVisual('fromSymbolSize', symbolSize && symbolSize[0]); data.setVisual('toSymbolSize', symbolSize && symbolSize[1]); data.setVisual('opacity', seriesModel.get(opacityQuery)); function dataEach(data, idx) { var itemModel = data.getItemModel(idx); var symbolType = normalize$2(itemModel.getShallow('symbol', true)); var symbolSize = normalize$2(itemModel.getShallow('symbolSize', true)); var opacity = itemModel.get(opacityQuery); symbolType[0] && data.setItemVisual(idx, 'fromSymbol', symbolType[0]); symbolType[1] && data.setItemVisual(idx, 'toSymbol', symbolType[1]); symbolSize[0] && data.setItemVisual(idx, 'fromSymbolSize', symbolSize[0]); symbolSize[1] && data.setItemVisual(idx, 'toSymbolSize', symbolSize[1]); data.setItemVisual(idx, 'opacity', opacity); } return {dataEach: data.hasItemOption ? dataEach : null}; } }; registerLayout(linesLayout); registerVisual(linesVisual); SeriesModel.extend({ type: 'series.heatmap', getInitialData: function (option, ecModel) { return createListFromArray(this.getSource(), this, { generateCoord: 'value' }); }, preventIncremental: function () { var coordSysCreator = CoordinateSystemManager.get(this.get('coordinateSystem')); if (coordSysCreator && coordSysCreator.dimensions) { return coordSysCreator.dimensions[0] === 'lng' && coordSysCreator.dimensions[1] === 'lat'; } }, defaultOption: { // Cartesian2D or geo coordinateSystem: 'cartesian2d', zlevel: 0, z: 2, // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, // Geo coordinate system geoIndex: 0, blurSize: 30, pointSize: 20, maxOpacity: 1, minOpacity: 0 } }); /** * @file defines echarts Heatmap Chart * @author Ovilia (me@zhangwenli.com) * Inspired by https://github.com/mourner/simpleheat * * @module */ var GRADIENT_LEVELS = 256; /** * Heatmap Chart * * @class */ function Heatmap() { var canvas = createCanvas(); this.canvas = canvas; this.blurSize = 30; this.pointSize = 20; this.maxOpacity = 1; this.minOpacity = 0; this._gradientPixels = {}; } Heatmap.prototype = { /** * Renders Heatmap and returns the rendered canvas * @param {Array} data array of data, each has x, y, value * @param {number} width canvas width * @param {number} height canvas height */ update: function(data, width, height, normalize, colorFunc, isInRange) { var brush = this._getBrush(); var gradientInRange = this._getGradient(data, colorFunc, 'inRange'); var gradientOutOfRange = this._getGradient(data, colorFunc, 'outOfRange'); var r = this.pointSize + this.blurSize; var canvas = this.canvas; var ctx = canvas.getContext('2d'); var len = data.length; canvas.width = width; canvas.height = height; for (var i = 0; i < len; ++i) { var p = data[i]; var x = p[0]; var y = p[1]; var value = p[2]; // calculate alpha using value var alpha = normalize(value); // draw with the circle brush with alpha ctx.globalAlpha = alpha; ctx.drawImage(brush, x - r, y - r); } if (!canvas.width || !canvas.height) { // Avoid "Uncaught DOMException: Failed to execute 'getImageData' on // 'CanvasRenderingContext2D': The source height is 0." return canvas; } // colorize the canvas using alpha value and set with gradient var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); var pixels = imageData.data; var offset = 0; var pixelLen = pixels.length; var minOpacity = this.minOpacity; var maxOpacity = this.maxOpacity; var diffOpacity = maxOpacity - minOpacity; while(offset < pixelLen) { var alpha = pixels[offset + 3] / 256; var gradientOffset = Math.floor(alpha * (GRADIENT_LEVELS - 1)) * 4; // Simple optimize to ignore the empty data if (alpha > 0) { var gradient = isInRange(alpha) ? gradientInRange : gradientOutOfRange; // Any alpha > 0 will be mapped to [minOpacity, maxOpacity] alpha > 0 && (alpha = alpha * diffOpacity + minOpacity); pixels[offset++] = gradient[gradientOffset]; pixels[offset++] = gradient[gradientOffset + 1]; pixels[offset++] = gradient[gradientOffset + 2]; pixels[offset++] = gradient[gradientOffset + 3] * alpha * 256; } else { offset += 4; } } ctx.putImageData(imageData, 0, 0); return canvas; }, /** * get canvas of a black circle brush used for canvas to draw later * @private * @returns {Object} circle brush canvas */ _getBrush: function() { var brushCanvas = this._brushCanvas || (this._brushCanvas = createCanvas()); // set brush size var r = this.pointSize + this.blurSize; var d = r * 2; brushCanvas.width = d; brushCanvas.height = d; var ctx = brushCanvas.getContext('2d'); ctx.clearRect(0, 0, d, d); // in order to render shadow without the distinct circle, // draw the distinct circle in an invisible place, // and use shadowOffset to draw shadow in the center of the canvas ctx.shadowOffsetX = d; ctx.shadowBlur = this.blurSize; // draw the shadow in black, and use alpha and shadow blur to generate // color in color map ctx.shadowColor = '#000'; // draw circle in the left to the canvas ctx.beginPath(); ctx.arc(-r, r, this.pointSize, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); return brushCanvas; }, /** * get gradient color map * @private */ _getGradient: function (data, colorFunc, state) { var gradientPixels = this._gradientPixels; var pixelsSingleState = gradientPixels[state] || (gradientPixels[state] = new Uint8ClampedArray(256 * 4)); var color = [0, 0, 0, 0]; var off = 0; for (var i = 0; i < 256; i++) { colorFunc[state](i / 255, true, color); pixelsSingleState[off++] = color[0]; pixelsSingleState[off++] = color[1]; pixelsSingleState[off++] = color[2]; pixelsSingleState[off++] = color[3]; } return pixelsSingleState; } }; function getIsInPiecewiseRange(dataExtent, pieceList, selected) { var dataSpan = dataExtent[1] - dataExtent[0]; pieceList = map(pieceList, function (piece) { return { interval: [ (piece.interval[0] - dataExtent[0]) / dataSpan, (piece.interval[1] - dataExtent[0]) / dataSpan ] }; }); var len = pieceList.length; var lastIndex = 0; return function (val) { // Try to find in the location of the last found for (var i = lastIndex; i < len; i++) { var interval = pieceList[i].interval; if (interval[0] <= val && val <= interval[1]) { lastIndex = i; break; } } if (i === len) { // Not found, back interation for (var i = lastIndex - 1; i >= 0; i--) { var interval = pieceList[i].interval; if (interval[0] <= val && val <= interval[1]) { lastIndex = i; break; } } } return i >= 0 && i < len && selected[i]; }; } function getIsInContinuousRange(dataExtent, range) { var dataSpan = dataExtent[1] - dataExtent[0]; range = [ (range[0] - dataExtent[0]) / dataSpan, (range[1] - dataExtent[0]) / dataSpan ]; return function (val) { return val >= range[0] && val <= range[1]; }; } function isGeoCoordSys(coordSys) { var dimensions = coordSys.dimensions; // Not use coorSys.type === 'geo' because coordSys maybe extended return dimensions[0] === 'lng' && dimensions[1] === 'lat'; } extendChartView({ type: 'heatmap', render: function (seriesModel, ecModel, api) { var visualMapOfThisSeries; ecModel.eachComponent('visualMap', function (visualMap) { visualMap.eachTargetSeries(function (targetSeries) { if (targetSeries === seriesModel) { visualMapOfThisSeries = visualMap; } }); }); if (__DEV__) { if (!visualMapOfThisSeries) { throw new Error('Heatmap must use with visualMap'); } } this.group.removeAll(); this._incrementalDisplayable = null; var coordSys = seriesModel.coordinateSystem; if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar') { this._renderOnCartesianAndCalendar(seriesModel, api, 0, seriesModel.getData().count()); } else if (isGeoCoordSys(coordSys)) { this._renderOnGeo( coordSys, seriesModel, visualMapOfThisSeries, api ); } }, incrementalPrepareRender: function (seriesModel, ecModel, api) { this.group.removeAll(); }, incrementalRender: function (params, seriesModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; if (coordSys) { this._renderOnCartesianAndCalendar(seriesModel, api, params.start, params.end, true); } }, _renderOnCartesianAndCalendar: function (seriesModel, api, start, end, incremental) { var coordSys = seriesModel.coordinateSystem; var width; var height; if (coordSys.type === 'cartesian2d') { var xAxis = coordSys.getAxis('x'); var yAxis = coordSys.getAxis('y'); if (__DEV__) { if (!(xAxis.type === 'category' && yAxis.type === 'category')) { throw new Error('Heatmap on cartesian must have two category axes'); } if (!(xAxis.onBand && yAxis.onBand)) { throw new Error('Heatmap on cartesian must have two axes with boundaryGap true'); } } width = xAxis.getBandWidth(); height = yAxis.getBandWidth(); } var group = this.group; var data = seriesModel.getData(); var itemStyleQuery = 'itemStyle'; var hoverItemStyleQuery = 'emphasis.itemStyle'; var labelQuery = 'label'; var hoverLabelQuery = 'emphasis.label'; var style = seriesModel.getModel(itemStyleQuery).getItemStyle(['color']); var hoverStl = seriesModel.getModel(hoverItemStyleQuery).getItemStyle(); var labelModel = seriesModel.getModel(labelQuery); var hoverLabelModel = seriesModel.getModel(hoverLabelQuery); var coordSysType = coordSys.type; var dataDims = coordSysType === 'cartesian2d' ? [ data.mapDimension('x'), data.mapDimension('y'), data.mapDimension('value') ] : [ data.mapDimension('time'), data.mapDimension('value') ]; for (var idx = start; idx < end; idx++) { var rect; if (coordSysType === 'cartesian2d') { // Ignore empty data if (isNaN(data.get(dataDims[2], idx))) { continue; } var point = coordSys.dataToPoint([ data.get(dataDims[0], idx), data.get(dataDims[1], idx) ]); rect = new Rect({ shape: { x: point[0] - width / 2, y: point[1] - height / 2, width: width, height: height }, style: { fill: data.getItemVisual(idx, 'color'), opacity: data.getItemVisual(idx, 'opacity') } }); } else { // Ignore empty data if (isNaN(data.get(dataDims[1], idx))) { continue; } rect = new Rect({ z2: 1, shape: coordSys.dataToRect([data.get(dataDims[0], idx)]).contentShape, style: { fill: data.getItemVisual(idx, 'color'), opacity: data.getItemVisual(idx, 'opacity') } }); } var itemModel = data.getItemModel(idx); // Optimization for large datset if (data.hasItemOption) { style = itemModel.getModel(itemStyleQuery).getItemStyle(['color']); hoverStl = itemModel.getModel(hoverItemStyleQuery).getItemStyle(); labelModel = itemModel.getModel(labelQuery); hoverLabelModel = itemModel.getModel(hoverLabelQuery); } var rawValue = seriesModel.getRawValue(idx); var defaultText = '-'; if (rawValue && rawValue[2] != null) { defaultText = rawValue[2]; } setLabelStyle( style, hoverStl, labelModel, hoverLabelModel, { labelFetcher: seriesModel, labelDataIndex: idx, defaultText: defaultText, isRectText: true } ); rect.setStyle(style); setHoverStyle(rect, data.hasItemOption ? hoverStl : extend({}, hoverStl)); rect.incremental = incremental; // PENDING if (incremental) { // Rect must use hover layer if it's incremental. rect.useHoverLayer = true; } group.add(rect); data.setItemGraphicEl(idx, rect); } }, _renderOnGeo: function (geo, seriesModel, visualMapModel, api) { var inRangeVisuals = visualMapModel.targetVisuals.inRange; var outOfRangeVisuals = visualMapModel.targetVisuals.outOfRange; // if (!visualMapping) { // throw new Error('Data range must have color visuals'); // } var data = seriesModel.getData(); var hmLayer = this._hmLayer || (this._hmLayer || new Heatmap()); hmLayer.blurSize = seriesModel.get('blurSize'); hmLayer.pointSize = seriesModel.get('pointSize'); hmLayer.minOpacity = seriesModel.get('minOpacity'); hmLayer.maxOpacity = seriesModel.get('maxOpacity'); var rect = geo.getViewRect().clone(); var roamTransform = geo.getRoamTransform(); rect.applyTransform(roamTransform); // Clamp on viewport var x = Math.max(rect.x, 0); var y = Math.max(rect.y, 0); var x2 = Math.min(rect.width + rect.x, api.getWidth()); var y2 = Math.min(rect.height + rect.y, api.getHeight()); var width = x2 - x; var height = y2 - y; var dims = [ data.mapDimension('lng'), data.mapDimension('lat'), data.mapDimension('value') ]; var points = data.mapArray(dims, function (lng, lat, value) { var pt = geo.dataToPoint([lng, lat]); pt[0] -= x; pt[1] -= y; pt.push(value); return pt; }); var dataExtent = visualMapModel.getExtent(); var isInRange = visualMapModel.type === 'visualMap.continuous' ? getIsInContinuousRange(dataExtent, visualMapModel.option.range) : getIsInPiecewiseRange( dataExtent, visualMapModel.getPieceList(), visualMapModel.option.selected ); hmLayer.update( points, width, height, inRangeVisuals.color.getNormalizer(), { inRange: inRangeVisuals.color.getColorMapper(), outOfRange: outOfRangeVisuals.color.getColorMapper() }, isInRange ); var img = new ZImage({ style: { width: width, height: height, x: x, y: y, image: hmLayer.canvas }, silent: true }); this.group.add(img); }, dispose: function () {} }); var PictorialBarSeries = BaseBarSeries.extend({ type: 'series.pictorialBar', dependencies: ['grid'], defaultOption: { symbol: 'circle', // Customized bar shape symbolSize: null, // Can be ['100%', '100%'], null means auto. symbolRotate: null, symbolPosition: null, // 'start' or 'end' or 'center', null means auto. symbolOffset: null, symbolMargin: null, // start margin and end margin. Can be a number or a percent string. // Auto margin by defualt. symbolRepeat: false, // false/null/undefined, means no repeat. // Can be true, means auto calculate repeat times and cut by data. // Can be a number, specifies repeat times, and do not cut by data. // Can be 'fixed', means auto calculate repeat times but do not cut by data. symbolRepeatDirection: 'end', // 'end' means from 'start' to 'end'. symbolClip: false, symbolBoundingData: null, // Can be 60 or -40 or [-40, 60] symbolPatternSize: 400, // 400 * 400 px barGap: '-100%', // In most case, overlap is needed. // z can be set in data item, which is z2 actually. // Disable progressive progressive: 0, hoverAnimation: false // Open only when needed. }, getInitialData: function (option) { // Disable stack. option.stack = null; return PictorialBarSeries.superApply(this, 'getInitialData', arguments); } }); var BAR_BORDER_WIDTH_QUERY$1 = ['itemStyle', 'borderWidth']; // index: +isHorizontal var LAYOUT_ATTRS = [ {xy: 'x', wh: 'width', index: 0, posDesc: ['left', 'right']}, {xy: 'y', wh: 'height', index: 1, posDesc: ['top', 'bottom']} ]; var pathForLineWidth = new Circle(); var BarView$1 = extendChartView({ type: 'pictorialBar', render: function (seriesModel, ecModel, api) { var group = this.group; var data = seriesModel.getData(); var oldData = this._data; var cartesian = seriesModel.coordinateSystem; var baseAxis = cartesian.getBaseAxis(); var isHorizontal = !!baseAxis.isHorizontal(); var coordSysRect = cartesian.grid.getRect(); var opt = { ecSize: {width: api.getWidth(), height: api.getHeight()}, seriesModel: seriesModel, coordSys: cartesian, coordSysExtent: [ [coordSysRect.x, coordSysRect.x + coordSysRect.width], [coordSysRect.y, coordSysRect.y + coordSysRect.height] ], isHorizontal: isHorizontal, valueDim: LAYOUT_ATTRS[+isHorizontal], categoryDim: LAYOUT_ATTRS[1 - isHorizontal] }; data.diff(oldData) .add(function (dataIndex) { if (!data.hasValue(dataIndex)) { return; } var itemModel = getItemModel(data, dataIndex); var symbolMeta = getSymbolMeta(data, dataIndex, itemModel, opt); var bar = createBar(data, opt, symbolMeta); data.setItemGraphicEl(dataIndex, bar); group.add(bar); updateCommon$1(bar, opt, symbolMeta); }) .update(function (newIndex, oldIndex) { var bar = oldData.getItemGraphicEl(oldIndex); if (!data.hasValue(newIndex)) { group.remove(bar); return; } var itemModel = getItemModel(data, newIndex); var symbolMeta = getSymbolMeta(data, newIndex, itemModel, opt); var pictorialShapeStr = getShapeStr(data, symbolMeta); if (bar && pictorialShapeStr !== bar.__pictorialShapeStr) { group.remove(bar); data.setItemGraphicEl(newIndex, null); bar = null; } if (bar) { updateBar(bar, opt, symbolMeta); } else { bar = createBar(data, opt, symbolMeta, true); } data.setItemGraphicEl(newIndex, bar); bar.__pictorialSymbolMeta = symbolMeta; // Add back group.add(bar); updateCommon$1(bar, opt, symbolMeta); }) .remove(function (dataIndex) { var bar = oldData.getItemGraphicEl(dataIndex); bar && removeBar(oldData, dataIndex, bar.__pictorialSymbolMeta.animationModel, bar); }) .execute(); this._data = data; return this.group; }, dispose: noop, remove: function (ecModel, api) { var group = this.group; var data = this._data; if (ecModel.get('animation')) { if (data) { data.eachItemGraphicEl(function (bar) { removeBar(data, bar.dataIndex, ecModel, bar); }); } } else { group.removeAll(); } } }); // Set or calculate default value about symbol, and calculate layout info. function getSymbolMeta(data, dataIndex, itemModel, opt) { var layout = data.getItemLayout(dataIndex); var symbolRepeat = itemModel.get('symbolRepeat'); var symbolClip = itemModel.get('symbolClip'); var symbolPosition = itemModel.get('symbolPosition') || 'start'; var symbolRotate = itemModel.get('symbolRotate'); var rotation = (symbolRotate || 0) * Math.PI / 180 || 0; var symbolPatternSize = itemModel.get('symbolPatternSize') || 2; var isAnimationEnabled = itemModel.isAnimationEnabled(); var symbolMeta = { dataIndex: dataIndex, layout: layout, itemModel: itemModel, symbolType: data.getItemVisual(dataIndex, 'symbol') || 'circle', color: data.getItemVisual(dataIndex, 'color'), symbolClip: symbolClip, symbolRepeat: symbolRepeat, symbolRepeatDirection: itemModel.get('symbolRepeatDirection'), symbolPatternSize: symbolPatternSize, rotation: rotation, animationModel: isAnimationEnabled ? itemModel : null, hoverAnimation: isAnimationEnabled && itemModel.get('hoverAnimation'), z2: itemModel.getShallow('z', true) || 0 }; prepareBarLength(itemModel, symbolRepeat, layout, opt, symbolMeta); prepareSymbolSize( data, dataIndex, layout, symbolRepeat, symbolClip, symbolMeta.boundingLength, symbolMeta.pxSign, symbolPatternSize, opt, symbolMeta ); prepareLineWidth(itemModel, symbolMeta.symbolScale, rotation, opt, symbolMeta); var symbolSize = symbolMeta.symbolSize; var symbolOffset = itemModel.get('symbolOffset'); if (isArray(symbolOffset)) { symbolOffset = [ parsePercent$1(symbolOffset[0], symbolSize[0]), parsePercent$1(symbolOffset[1], symbolSize[1]) ]; } prepareLayoutInfo( itemModel, symbolSize, layout, symbolRepeat, symbolClip, symbolOffset, symbolPosition, symbolMeta.valueLineWidth, symbolMeta.boundingLength, symbolMeta.repeatCutLength, opt, symbolMeta ); return symbolMeta; } // bar length can be negative. function prepareBarLength(itemModel, symbolRepeat, layout, opt, output) { var valueDim = opt.valueDim; var symbolBoundingData = itemModel.get('symbolBoundingData'); var valueAxis = opt.coordSys.getOtherAxis(opt.coordSys.getBaseAxis()); var zeroPx = valueAxis.toGlobalCoord(valueAxis.dataToCoord(0)); var pxSignIdx = 1 - +(layout[valueDim.wh] <= 0); var boundingLength; if (isArray(symbolBoundingData)) { var symbolBoundingExtent = [ convertToCoordOnAxis(valueAxis, symbolBoundingData[0]) - zeroPx, convertToCoordOnAxis(valueAxis, symbolBoundingData[1]) - zeroPx ]; symbolBoundingExtent[1] < symbolBoundingExtent[0] && (symbolBoundingExtent.reverse()); boundingLength = symbolBoundingExtent[pxSignIdx]; } else if (symbolBoundingData != null) { boundingLength = convertToCoordOnAxis(valueAxis, symbolBoundingData) - zeroPx; } else if (symbolRepeat) { boundingLength = opt.coordSysExtent[valueDim.index][pxSignIdx] - zeroPx; } else { boundingLength = layout[valueDim.wh]; } output.boundingLength = boundingLength; if (symbolRepeat) { output.repeatCutLength = layout[valueDim.wh]; } output.pxSign = boundingLength > 0 ? 1 : boundingLength < 0 ? -1 : 0; } function convertToCoordOnAxis(axis, value) { return axis.toGlobalCoord(axis.dataToCoord(axis.scale.parse(value))); } // Support ['100%', '100%'] function prepareSymbolSize( data, dataIndex, layout, symbolRepeat, symbolClip, boundingLength, pxSign, symbolPatternSize, opt, output ) { var valueDim = opt.valueDim; var categoryDim = opt.categoryDim; var categorySize = Math.abs(layout[categoryDim.wh]); var symbolSize = data.getItemVisual(dataIndex, 'symbolSize'); if (isArray(symbolSize)) { symbolSize = symbolSize.slice(); } else { if (symbolSize == null) { symbolSize = '100%'; } symbolSize = [symbolSize, symbolSize]; } // Note: percentage symbolSize (like '100%') do not consider lineWidth, because it is // to complicated to calculate real percent value if considering scaled lineWidth. // So the actual size will bigger than layout size if lineWidth is bigger than zero, // which can be tolerated in pictorial chart. symbolSize[categoryDim.index] = parsePercent$1( symbolSize[categoryDim.index], categorySize ); symbolSize[valueDim.index] = parsePercent$1( symbolSize[valueDim.index], symbolRepeat ? categorySize : Math.abs(boundingLength) ); output.symbolSize = symbolSize; // If x or y is less than zero, show reversed shape. var symbolScale = output.symbolScale = [ symbolSize[0] / symbolPatternSize, symbolSize[1] / symbolPatternSize ]; // Follow convention, 'right' and 'top' is the normal scale. symbolScale[valueDim.index] *= (opt.isHorizontal ? -1 : 1) * pxSign; } function prepareLineWidth(itemModel, symbolScale, rotation, opt, output) { // In symbols are drawn with scale, so do not need to care about the case that width // or height are too small. But symbol use strokeNoScale, where acture lineWidth should // be calculated. var valueLineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY$1) || 0; if (valueLineWidth) { pathForLineWidth.attr({ scale: symbolScale.slice(), rotation: rotation }); pathForLineWidth.updateTransform(); valueLineWidth /= pathForLineWidth.getLineScale(); valueLineWidth *= symbolScale[opt.valueDim.index]; } output.valueLineWidth = valueLineWidth; } function prepareLayoutInfo( itemModel, symbolSize, layout, symbolRepeat, symbolClip, symbolOffset, symbolPosition, valueLineWidth, boundingLength, repeatCutLength, opt, output ) { var categoryDim = opt.categoryDim; var valueDim = opt.valueDim; var pxSign = output.pxSign; var unitLength = Math.max(symbolSize[valueDim.index] + valueLineWidth, 0); var pathLen = unitLength; // Note: rotation will not effect the layout of symbols, because user may // want symbols to rotate on its center, which should not be translated // when rotating. if (symbolRepeat) { var absBoundingLength = Math.abs(boundingLength); var symbolMargin = retrieve(itemModel.get('symbolMargin'), '15%') + ''; var hasEndGap = false; if (symbolMargin.lastIndexOf('!') === symbolMargin.length - 1) { hasEndGap = true; symbolMargin = symbolMargin.slice(0, symbolMargin.length - 1); } symbolMargin = parsePercent$1(symbolMargin, symbolSize[valueDim.index]); var uLenWithMargin = Math.max(unitLength + symbolMargin * 2, 0); // When symbol margin is less than 0, margin at both ends will be subtracted // to ensure that all of the symbols will not be overflow the given area. var endFix = hasEndGap ? 0 : symbolMargin * 2; // Both final repeatTimes and final symbolMargin area calculated based on // boundingLength. var repeatSpecified = isNumeric(symbolRepeat); var repeatTimes = repeatSpecified ? symbolRepeat : toIntTimes((absBoundingLength + endFix) / uLenWithMargin); // Adjust calculate margin, to ensure each symbol is displayed // entirely in the given layout area. var mDiff = absBoundingLength - repeatTimes * unitLength; symbolMargin = mDiff / 2 / (hasEndGap ? repeatTimes : repeatTimes - 1); uLenWithMargin = unitLength + symbolMargin * 2; endFix = hasEndGap ? 0 : symbolMargin * 2; // Update repeatTimes when not all symbol will be shown. if (!repeatSpecified && symbolRepeat !== 'fixed') { repeatTimes = repeatCutLength ? toIntTimes((Math.abs(repeatCutLength) + endFix) / uLenWithMargin) : 0; } pathLen = repeatTimes * uLenWithMargin - endFix; output.repeatTimes = repeatTimes; output.symbolMargin = symbolMargin; } var sizeFix = pxSign * (pathLen / 2); var pathPosition = output.pathPosition = []; pathPosition[categoryDim.index] = layout[categoryDim.wh] / 2; pathPosition[valueDim.index] = symbolPosition === 'start' ? sizeFix : symbolPosition === 'end' ? boundingLength - sizeFix : boundingLength / 2; // 'center' if (symbolOffset) { pathPosition[0] += symbolOffset[0]; pathPosition[1] += symbolOffset[1]; } var bundlePosition = output.bundlePosition = []; bundlePosition[categoryDim.index] = layout[categoryDim.xy]; bundlePosition[valueDim.index] = layout[valueDim.xy]; var barRectShape = output.barRectShape = extend({}, layout); barRectShape[valueDim.wh] = pxSign * Math.max( Math.abs(layout[valueDim.wh]), Math.abs(pathPosition[valueDim.index] + sizeFix) ); barRectShape[categoryDim.wh] = layout[categoryDim.wh]; var clipShape = output.clipShape = {}; // Consider that symbol may be overflow layout rect. clipShape[categoryDim.xy] = -layout[categoryDim.xy]; clipShape[categoryDim.wh] = opt.ecSize[categoryDim.wh]; clipShape[valueDim.xy] = 0; clipShape[valueDim.wh] = layout[valueDim.wh]; } function createPath(symbolMeta) { var symbolPatternSize = symbolMeta.symbolPatternSize; var path = createSymbol( // Consider texture img, make a big size. symbolMeta.symbolType, -symbolPatternSize / 2, -symbolPatternSize / 2, symbolPatternSize, symbolPatternSize, symbolMeta.color ); path.attr({ culling: true }); path.type !== 'image' && path.setStyle({ strokeNoScale: true }); return path; } function createOrUpdateRepeatSymbols(bar, opt, symbolMeta, isUpdate) { var bundle = bar.__pictorialBundle; var symbolSize = symbolMeta.symbolSize; var valueLineWidth = symbolMeta.valueLineWidth; var pathPosition = symbolMeta.pathPosition; var valueDim = opt.valueDim; var repeatTimes = symbolMeta.repeatTimes || 0; var index = 0; var unit = symbolSize[opt.valueDim.index] + valueLineWidth + symbolMeta.symbolMargin * 2; eachPath(bar, function (path) { path.__pictorialAnimationIndex = index; path.__pictorialRepeatTimes = repeatTimes; if (index < repeatTimes) { updateAttr(path, null, makeTarget(index), symbolMeta, isUpdate); } else { updateAttr(path, null, {scale: [0, 0]}, symbolMeta, isUpdate, function () { bundle.remove(path); }); } updateHoverAnimation(path, symbolMeta); index++; }); for (; index < repeatTimes; index++) { var path = createPath(symbolMeta); path.__pictorialAnimationIndex = index; path.__pictorialRepeatTimes = repeatTimes; bundle.add(path); var target = makeTarget(index); updateAttr( path, { position: target.position, scale: [0, 0] }, { scale: target.scale, rotation: target.rotation }, symbolMeta, isUpdate ); // FIXME // If all emphasis/normal through action. path .on('mouseover', onMouseOver) .on('mouseout', onMouseOut); updateHoverAnimation(path, symbolMeta); } function makeTarget(index) { var position = pathPosition.slice(); // (start && pxSign > 0) || (end && pxSign < 0): i = repeatTimes - index // Otherwise: i = index; var pxSign = symbolMeta.pxSign; var i = index; if (symbolMeta.symbolRepeatDirection === 'start' ? pxSign > 0 : pxSign < 0) { i = repeatTimes - 1 - index; } position[valueDim.index] = unit * (i - repeatTimes / 2 + 0.5) + pathPosition[valueDim.index]; return { position: position, scale: symbolMeta.symbolScale.slice(), rotation: symbolMeta.rotation }; } function onMouseOver() { eachPath(bar, function (path) { path.trigger('emphasis'); }); } function onMouseOut() { eachPath(bar, function (path) { path.trigger('normal'); }); } } function createOrUpdateSingleSymbol(bar, opt, symbolMeta, isUpdate) { var bundle = bar.__pictorialBundle; var mainPath = bar.__pictorialMainPath; if (!mainPath) { mainPath = bar.__pictorialMainPath = createPath(symbolMeta); bundle.add(mainPath); updateAttr( mainPath, { position: symbolMeta.pathPosition.slice(), scale: [0, 0], rotation: symbolMeta.rotation }, { scale: symbolMeta.symbolScale.slice() }, symbolMeta, isUpdate ); mainPath .on('mouseover', onMouseOver) .on('mouseout', onMouseOut); } else { updateAttr( mainPath, null, { position: symbolMeta.pathPosition.slice(), scale: symbolMeta.symbolScale.slice(), rotation: symbolMeta.rotation }, symbolMeta, isUpdate ); } updateHoverAnimation(mainPath, symbolMeta); function onMouseOver() { this.trigger('emphasis'); } function onMouseOut() { this.trigger('normal'); } } // bar rect is used for label. function createOrUpdateBarRect(bar, symbolMeta, isUpdate) { var rectShape = extend({}, symbolMeta.barRectShape); var barRect = bar.__pictorialBarRect; if (!barRect) { barRect = bar.__pictorialBarRect = new Rect({ z2: 2, shape: rectShape, silent: true, style: { stroke: 'transparent', fill: 'transparent', lineWidth: 0 } }); bar.add(barRect); } else { updateAttr(barRect, null, {shape: rectShape}, symbolMeta, isUpdate); } } function createOrUpdateClip(bar, opt, symbolMeta, isUpdate) { // If not clip, symbol will be remove and rebuilt. if (symbolMeta.symbolClip) { var clipPath = bar.__pictorialClipPath; var clipShape = extend({}, symbolMeta.clipShape); var valueDim = opt.valueDim; var animationModel = symbolMeta.animationModel; var dataIndex = symbolMeta.dataIndex; if (clipPath) { updateProps( clipPath, {shape: clipShape}, animationModel, dataIndex ); } else { clipShape[valueDim.wh] = 0; clipPath = new Rect({shape: clipShape}); bar.__pictorialBundle.setClipPath(clipPath); bar.__pictorialClipPath = clipPath; var target = {}; target[valueDim.wh] = symbolMeta.clipShape[valueDim.wh]; graphic[isUpdate ? 'updateProps' : 'initProps']( clipPath, {shape: target}, animationModel, dataIndex ); } } } function getItemModel(data, dataIndex) { var itemModel = data.getItemModel(dataIndex); itemModel.getAnimationDelayParams = getAnimationDelayParams; itemModel.isAnimationEnabled = isAnimationEnabled; return itemModel; } function getAnimationDelayParams(path) { // The order is the same as the z-order, see `symbolRepeatDiretion`. return { index: path.__pictorialAnimationIndex, count: path.__pictorialRepeatTimes }; } function isAnimationEnabled() { // `animation` prop can be set on itemModel in pictorial bar chart. return this.parentModel.isAnimationEnabled() && !!this.getShallow('animation'); } function updateHoverAnimation(path, symbolMeta) { path.off('emphasis').off('normal'); var scale = symbolMeta.symbolScale.slice(); symbolMeta.hoverAnimation && path .on('emphasis', function() { this.animateTo({ scale: [scale[0] * 1.1, scale[1] * 1.1] }, 400, 'elasticOut'); }) .on('normal', function() { this.animateTo({ scale: scale.slice() }, 400, 'elasticOut'); }); } function createBar(data, opt, symbolMeta, isUpdate) { // bar is the main element for each data. var bar = new Group(); // bundle is used for location and clip. var bundle = new Group(); bar.add(bundle); bar.__pictorialBundle = bundle; bundle.attr('position', symbolMeta.bundlePosition.slice()); if (symbolMeta.symbolRepeat) { createOrUpdateRepeatSymbols(bar, opt, symbolMeta); } else { createOrUpdateSingleSymbol(bar, opt, symbolMeta); } createOrUpdateBarRect(bar, symbolMeta, isUpdate); createOrUpdateClip(bar, opt, symbolMeta, isUpdate); bar.__pictorialShapeStr = getShapeStr(data, symbolMeta); bar.__pictorialSymbolMeta = symbolMeta; return bar; } function updateBar(bar, opt, symbolMeta) { var animationModel = symbolMeta.animationModel; var dataIndex = symbolMeta.dataIndex; var bundle = bar.__pictorialBundle; updateProps( bundle, {position: symbolMeta.bundlePosition.slice()}, animationModel, dataIndex ); if (symbolMeta.symbolRepeat) { createOrUpdateRepeatSymbols(bar, opt, symbolMeta, true); } else { createOrUpdateSingleSymbol(bar, opt, symbolMeta, true); } createOrUpdateBarRect(bar, symbolMeta, true); createOrUpdateClip(bar, opt, symbolMeta, true); } function removeBar(data, dataIndex, animationModel, bar) { // Not show text when animating var labelRect = bar.__pictorialBarRect; labelRect && (labelRect.style.text = null); var pathes = []; eachPath(bar, function (path) { pathes.push(path); }); bar.__pictorialMainPath && pathes.push(bar.__pictorialMainPath); // I do not find proper remove animation for clip yet. bar.__pictorialClipPath && (animationModel = null); each$1(pathes, function (path) { updateProps( path, {scale: [0, 0]}, animationModel, dataIndex, function () { bar.parent && bar.parent.remove(bar); } ); }); data.setItemGraphicEl(dataIndex, null); } function getShapeStr(data, symbolMeta) { return [ data.getItemVisual(symbolMeta.dataIndex, 'symbol') || 'none', !!symbolMeta.symbolRepeat, !!symbolMeta.symbolClip ].join(':'); } function eachPath(bar, cb, context) { // Do not use Group#eachChild, because it do not support remove. each$1(bar.__pictorialBundle.children(), function (el) { el !== bar.__pictorialBarRect && cb.call(context, el); }); } function updateAttr(el, immediateAttrs, animationAttrs, symbolMeta, isUpdate, cb) { immediateAttrs && el.attr(immediateAttrs); // when symbolCip used, only clip path has init animation, otherwise it would be weird effect. if (symbolMeta.symbolClip && !isUpdate) { animationAttrs && el.attr(animationAttrs); } else { animationAttrs && graphic[isUpdate ? 'updateProps' : 'initProps']( el, animationAttrs, symbolMeta.animationModel, symbolMeta.dataIndex, cb ); } } function updateCommon$1(bar, opt, symbolMeta) { var color = symbolMeta.color; var dataIndex = symbolMeta.dataIndex; var itemModel = symbolMeta.itemModel; // Color must be excluded. // Because symbol provide setColor individually to set fill and stroke var normalStyle = itemModel.getModel('itemStyle').getItemStyle(['color']); var hoverStyle = itemModel.getModel('emphasis.itemStyle').getItemStyle(); var cursorStyle = itemModel.getShallow('cursor'); eachPath(bar, function (path) { // PENDING setColor should be before setStyle!!! path.setColor(color); path.setStyle(defaults( { fill: color, opacity: symbolMeta.opacity }, normalStyle )); setHoverStyle(path, hoverStyle); cursorStyle && (path.cursor = cursorStyle); path.z2 = symbolMeta.z2; }); var barRectHoverStyle = {}; var barPositionOutside = opt.valueDim.posDesc[+(symbolMeta.boundingLength > 0)]; var barRect = bar.__pictorialBarRect; setLabel( barRect.style, barRectHoverStyle, itemModel, color, opt.seriesModel, dataIndex, barPositionOutside ); setHoverStyle(barRect, barRectHoverStyle); } function toIntTimes(times) { var roundedTimes = Math.round(times); // Escapse accurate error return Math.abs(times - roundedTimes) < 1e-4 ? roundedTimes : Math.ceil(times); } // In case developer forget to include grid component registerLayout(curry( layout, 'pictorialBar' )); registerVisual(visualSymbol('pictorialBar', 'roundRect')); /** * @constructor module:echarts/coord/single/SingleAxis * @extends {module:echarts/coord/Axis} * @param {string} dim * @param {*} scale * @param {Array.<number>} coordExtent * @param {string} axisType * @param {string} position */ var SingleAxis = function (dim, scale, coordExtent, axisType, position) { Axis.call(this, dim, scale, coordExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = axisType || 'value'; /** * Axis position * - 'top' * - 'bottom' * - 'left' * - 'right' * @type {string} */ this.position = position || 'bottom'; /** * Axis orient * - 'horizontal' * - 'vertical' * @type {[type]} */ this.orient = null; /** * @type {number} */ this._labelInterval = null; }; SingleAxis.prototype = { constructor: SingleAxis, /** * Axis model * @type {module:echarts/coord/single/AxisModel} */ model: null, /** * Judge the orient of the axis. * @return {boolean} */ isHorizontal: function () { var position = this.position; return position === 'top' || position === 'bottom'; }, /** * @override */ pointToData: function (point, clamp) { return this.coordinateSystem.pointToData(point, clamp)[0]; }, /** * Convert the local coord(processed by dataToCoord()) * to global coord(concrete pixel coord). * designated by module:echarts/coord/single/Single. * @type {Function} */ toGlobalCoord: null, /** * Convert the global coord to local coord. * designated by module:echarts/coord/single/Single. * @type {Function} */ toLocalCoord: null }; inherits(SingleAxis, Axis); /** * Single coordinates system. */ /** * Create a single coordinates system. * * @param {module:echarts/coord/single/AxisModel} axisModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ function Single(axisModel, ecModel, api) { /** * @type {string} * @readOnly */ this.dimension = 'single'; /** * Add it just for draw tooltip. * * @type {Array.<string>} * @readOnly */ this.dimensions = ['single']; /** * @private * @type {module:echarts/coord/single/SingleAxis}. */ this._axis = null; /** * @private * @type {module:zrender/core/BoundingRect} */ this._rect; this._init(axisModel, ecModel, api); /** * @type {module:echarts/coord/single/AxisModel} */ this.model = axisModel; } Single.prototype = { type: 'singleAxis', axisPointerEnabled: true, constructor: Single, /** * Initialize single coordinate system. * * @param {module:echarts/coord/single/AxisModel} axisModel * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @private */ _init: function (axisModel, ecModel, api) { var dim = this.dimension; var axis = new SingleAxis( dim, createScaleByModel(axisModel), [0, 0], axisModel.get('type'), axisModel.get('position') ); var isCategory = axis.type === 'category'; axis.onBand = isCategory && axisModel.get('boundaryGap'); axis.inverse = axisModel.get('inverse'); axis.orient = axisModel.get('orient'); axisModel.axis = axis; axis.model = axisModel; axis.coordinateSystem = this; this._axis = axis; }, /** * Update axis scale after data processed * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ update: function (ecModel, api) { ecModel.eachSeries(function (seriesModel) { if (seriesModel.coordinateSystem === this) { var data = seriesModel.getData(); each$1(data.mapDimension(this.dimension, true), function (dim) { this._axis.scale.unionExtentFromData(data, dim); }, this); niceScaleExtent(this._axis.scale, this._axis.model); } }, this); }, /** * Resize the single coordinate system. * * @param {module:echarts/coord/single/AxisModel} axisModel * @param {module:echarts/ExtensionAPI} api */ resize: function (axisModel, api) { this._rect = getLayoutRect( { left: axisModel.get('left'), top: axisModel.get('top'), right: axisModel.get('right'), bottom: axisModel.get('bottom'), width: axisModel.get('width'), height: axisModel.get('height') }, { width: api.getWidth(), height: api.getHeight() } ); this._adjustAxis(); }, /** * @return {module:zrender/core/BoundingRect} */ getRect: function () { return this._rect; }, /** * @private */ _adjustAxis: function () { var rect = this._rect; var axis = this._axis; var isHorizontal = axis.isHorizontal(); var extent = isHorizontal ? [0, rect.width] : [0, rect.height]; var idx = axis.reverse ? 1 : 0; axis.setExtent(extent[idx], extent[1 - idx]); this._updateAxisTransform(axis, isHorizontal ? rect.x : rect.y); }, /** * @param {module:echarts/coord/single/SingleAxis} axis * @param {number} coordBase */ _updateAxisTransform: function (axis, coordBase) { var axisExtent = axis.getExtent(); var extentSum = axisExtent[0] + axisExtent[1]; var isHorizontal = axis.isHorizontal(); axis.toGlobalCoord = isHorizontal ? function (coord) { return coord + coordBase; } : function (coord) { return extentSum - coord + coordBase; }; axis.toLocalCoord = isHorizontal ? function (coord) { return coord - coordBase; } : function (coord) { return extentSum - coord + coordBase; }; }, /** * Get axis. * * @return {module:echarts/coord/single/SingleAxis} */ getAxis: function () { return this._axis; }, /** * Get axis, add it just for draw tooltip. * * @return {[type]} [description] */ getBaseAxis: function () { return this._axis; }, /** * @return {Array.<module:echarts/coord/Axis>} */ getAxes: function () { return [this._axis]; }, /** * @return {Object} {baseAxes: [], otherAxes: []} */ getTooltipAxes: function () { return {baseAxes: [this.getAxis()]}; }, /** * If contain point. * * @param {Array.<number>} point * @return {boolean} */ containPoint: function (point) { var rect = this.getRect(); var axis = this.getAxis(); var orient = axis.orient; if (orient === 'horizontal') { return axis.contain(axis.toLocalCoord(point[0])) && (point[1] >= rect.y && point[1] <= (rect.y + rect.height)); } else { return axis.contain(axis.toLocalCoord(point[1])) && (point[0] >= rect.y && point[0] <= (rect.y + rect.height)); } }, /** * @param {Array.<number>} point * @return {Array.<number>} */ pointToData: function (point) { var axis = this.getAxis(); return [axis.coordToData(axis.toLocalCoord( point[axis.orient === 'horizontal' ? 0 : 1] ))]; }, /** * Convert the series data to concrete point. * * @param {number|Array.<number>} val * @return {Array.<number>} */ dataToPoint: function (val) { var axis = this.getAxis(); var rect = this.getRect(); var pt = []; var idx = axis.orient === 'horizontal' ? 0 : 1; if (val instanceof Array) { val = val[0]; } pt[idx] = axis.toGlobalCoord(axis.dataToCoord(+val)); pt[1 - idx] = idx === 0 ? (rect.y + rect.height / 2) : (rect.x + rect.width / 2); return pt; } }; /** * Single coordinate system creator. */ /** * Create single coordinate system and inject it into seriesModel. * * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @return {Array.<module:echarts/coord/single/Single>} */ function create$3(ecModel, api) { var singles = []; ecModel.eachComponent('singleAxis', function(axisModel, idx) { var single = new Single(axisModel, ecModel, api); single.name = 'single_' + idx; single.resize(axisModel, api); axisModel.coordinateSystem = single; singles.push(single); }); ecModel.eachSeries(function (seriesModel) { if (seriesModel.get('coordinateSystem') === 'singleAxis') { var singleAxisModel = ecModel.queryComponents({ mainType: 'singleAxis', index: seriesModel.get('singleAxisIndex'), id: seriesModel.get('singleAxisId') })[0]; seriesModel.coordinateSystem = singleAxisModel && singleAxisModel.coordinateSystem; } }); return singles; } CoordinateSystemManager.register('single', { create: create$3, dimensions: Single.prototype.dimensions }); /** * @param {Object} opt {labelInside} * @return {Object} { * position, rotation, labelDirection, labelOffset, * tickDirection, labelRotate, labelInterval, z2 * } */ function layout$2 (axisModel, opt) { opt = opt || {}; var single = axisModel.coordinateSystem; var axis = axisModel.axis; var layout = {}; var axisPosition = axis.position; var orient = axis.orient; var rect = single.getRect(); var rectBound = [rect.x, rect.x + rect.width, rect.y, rect.y + rect.height]; var positionMap = { horizontal: {top: rectBound[2], bottom: rectBound[3]}, vertical: {left: rectBound[0], right: rectBound[1]} }; layout.position = [ orient === 'vertical' ? positionMap.vertical[axisPosition] : rectBound[0], orient === 'horizontal' ? positionMap.horizontal[axisPosition] : rectBound[3] ]; var r = {horizontal: 0, vertical: 1}; layout.rotation = Math.PI / 2 * r[orient]; var directionMap = {top: -1, bottom: 1, right: 1, left: -1}; layout.labelDirection = layout.tickDirection = layout.nameDirection = directionMap[axisPosition]; if (axisModel.get('axisTick.inside')) { layout.tickDirection = -layout.tickDirection; } if (retrieve(opt.labelInside, axisModel.get('axisLabel.inside'))) { layout.labelDirection = -layout.labelDirection; } var labelRotation = opt.rotate; labelRotation == null && (labelRotation = axisModel.get('axisLabel.rotate')); layout.labelRotation = axisPosition === 'top' ? -labelRotation : labelRotation; layout.labelInterval = axis.getLabelInterval(); layout.z2 = 1; return layout; } var getInterval$2 = AxisBuilder.getInterval; var ifIgnoreOnTick$2 = AxisBuilder.ifIgnoreOnTick; var axisBuilderAttrs$2 = [ 'axisLine', 'axisTickLabel', 'axisName' ]; var selfBuilderAttr = 'splitLine'; var SingleAxisView = AxisView.extend({ type: 'singleAxis', axisPointerClass: 'SingleAxisPointer', render: function (axisModel, ecModel, api, payload) { var group = this.group; group.removeAll(); var layout = layout$2(axisModel); var axisBuilder = new AxisBuilder(axisModel, layout); each$1(axisBuilderAttrs$2, axisBuilder.add, axisBuilder); group.add(axisBuilder.getGroup()); if (axisModel.get(selfBuilderAttr + '.show')) { this['_' + selfBuilderAttr](axisModel, layout.labelInterval); } SingleAxisView.superCall(this, 'render', axisModel, ecModel, api, payload); }, _splitLine: function(axisModel, labelInterval) { var axis = axisModel.axis; if (axis.scale.isBlank()) { return; } var splitLineModel = axisModel.getModel('splitLine'); var lineStyleModel = splitLineModel.getModel('lineStyle'); var lineWidth = lineStyleModel.get('width'); var lineColors = lineStyleModel.get('color'); var lineInterval = getInterval$2(splitLineModel, labelInterval); lineColors = lineColors instanceof Array ? lineColors : [lineColors]; var gridRect = axisModel.coordinateSystem.getRect(); var isHorizontal = axis.isHorizontal(); var splitLines = []; var lineCount = 0; var ticksCoords = axis.getTicksCoords(); var p1 = []; var p2 = []; var showMinLabel = axisModel.get('axisLabel.showMinLabel'); var showMaxLabel = axisModel.get('axisLabel.showMaxLabel'); for (var i = 0; i < ticksCoords.length; ++i) { if (ifIgnoreOnTick$2( axis, i, lineInterval, ticksCoords.length, showMinLabel, showMaxLabel )) { continue; } var tickCoord = axis.toGlobalCoord(ticksCoords[i]); if (isHorizontal) { p1[0] = tickCoord; p1[1] = gridRect.y; p2[0] = tickCoord; p2[1] = gridRect.y + gridRect.height; } else { p1[0] = gridRect.x; p1[1] = tickCoord; p2[0] = gridRect.x + gridRect.width; p2[1] = tickCoord; } var colorIndex = (lineCount++) % lineColors.length; splitLines[colorIndex] = splitLines[colorIndex] || []; splitLines[colorIndex].push(new Line( subPixelOptimizeLine({ shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] }, style: { lineWidth: lineWidth }, silent: true }))); } for (var i = 0; i < splitLines.length; ++i) { this.group.add(mergePath(splitLines[i], { style: { stroke: lineColors[i % lineColors.length], lineDash: lineStyleModel.getLineDash(lineWidth), lineWidth: lineWidth }, silent: true })); } } }); var AxisModel$4 = ComponentModel.extend({ type: 'singleAxis', layoutMode: 'box', /** * @type {module:echarts/coord/single/SingleAxis} */ axis: null, /** * @type {module:echarts/coord/single/Single} */ coordinateSystem: null, /** * @override */ getCoordSysModel: function () { return this; } }); var defaultOption$2 = { left: '5%', top: '5%', right: '5%', bottom: '5%', type: 'value', position: 'bottom', orient: 'horizontal', axisLine: { show: true, lineStyle: { width: 2, type: 'solid' } }, // Single coordinate system and single axis is the, // which is used as the parent tooltip model. // same model, so we set default tooltip show as true. tooltip: { show: true }, axisTick: { show: true, length: 6, lineStyle: { width: 2 } }, axisLabel: { show: true, interval: 'auto' }, splitLine: { show: true, lineStyle: { type: 'dashed', opacity: 0.2 } } }; function getAxisType$2(axisName, option) { return option.type || (option.data ? 'category' : 'value'); } merge(AxisModel$4.prototype, axisModelCommonMixin); axisModelCreator('single', AxisModel$4, getAxisType$2, defaultOption$2); /** * @param {Object} finder contains {seriesIndex, dataIndex, dataIndexInside} * @param {module:echarts/model/Global} ecModel * @return {Object} {point: [x, y], el: ...} point Will not be null. */ var findPointFromSeries = function (finder, ecModel) { var point = []; var seriesIndex = finder.seriesIndex; var seriesModel; if (seriesIndex == null || !( seriesModel = ecModel.getSeriesByIndex(seriesIndex) )) { return {point: []}; } var data = seriesModel.getData(); var dataIndex = queryDataIndex(data, finder); if (dataIndex == null || dataIndex < 0 || isArray(dataIndex)) { return {point: []}; } var el = data.getItemGraphicEl(dataIndex); var coordSys = seriesModel.coordinateSystem; if (seriesModel.getTooltipPosition) { point = seriesModel.getTooltipPosition(dataIndex) || []; } else if (coordSys && coordSys.dataToPoint) { point = coordSys.dataToPoint( data.getValues( map(coordSys.dimensions, function (dim) { return data.mapDimension(dim); }), dataIndex, true ) ) || []; } else if (el) { // Use graphic bounding rect var rect = el.getBoundingRect().clone(); rect.applyTransform(el.transform); point = [ rect.x + rect.width / 2, rect.y + rect.height / 2 ]; } return {point: point, el: el}; }; var each$15 = each$1; var curry$3 = curry; var inner$6 = makeInner(); /** * Basic logic: check all axis, if they do not demand show/highlight, * then hide/downplay them. * * @param {Object} coordSysAxesInfo * @param {Object} payload * @param {string} [payload.currTrigger] 'click' | 'mousemove' | 'leave' * @param {Array.<number>} [payload.x] x and y, which are mandatory, specify a point to * trigger axisPointer and tooltip. * @param {Array.<number>} [payload.y] x and y, which are mandatory, specify a point to * trigger axisPointer and tooltip. * @param {Object} [payload.seriesIndex] finder, optional, restrict target axes. * @param {Object} [payload.dataIndex] finder, restrict target axes. * @param {Object} [payload.axesInfo] finder, restrict target axes. * [{ * axisDim: 'x'|'y'|'angle'|..., * axisIndex: ..., * value: ... * }, ...] * @param {Function} [payload.dispatchAction] * @param {Object} [payload.tooltipOption] * @param {Object|Array.<number>|Function} [payload.position] Tooltip position, * which can be specified in dispatchAction * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api * @return {Object} content of event obj for echarts.connect. */ var axisTrigger = function (payload, ecModel, api) { var currTrigger = payload.currTrigger; var point = [payload.x, payload.y]; var finder = payload; var dispatchAction = payload.dispatchAction || bind(api.dispatchAction, api); var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo; // Pending // See #6121. But we are not able to reproduce it yet. if (!coordSysAxesInfo) { return; } if (illegalPoint(point)) { // Used in the default behavior of `connection`: use the sample seriesIndex // and dataIndex. And also used in the tooltipView trigger. point = findPointFromSeries({ seriesIndex: finder.seriesIndex, // Do not use dataIndexInside from other ec instance. // FIXME: auto detect it? dataIndex: finder.dataIndex }, ecModel).point; } var isIllegalPoint = illegalPoint(point); // Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}). // Notice: In this case, it is difficult to get the `point` (which is necessary to show // tooltip, so if point is not given, we just use the point found by sample seriesIndex // and dataIndex. var inputAxesInfo = finder.axesInfo; var axesInfo = coordSysAxesInfo.axesInfo; var shouldHide = currTrigger === 'leave' || illegalPoint(point); var outputFinder = {}; var showValueMap = {}; var dataByCoordSys = {list: [], map: {}}; var updaters = { showPointer: curry$3(showPointer, showValueMap), showTooltip: curry$3(showTooltip, dataByCoordSys) }; // Process for triggered axes. each$15(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) { // If a point given, it must be contained by the coordinate system. var coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point); each$15(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) { var axis = axisInfo.axis; var inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo); // If no inputAxesInfo, no axis is restricted. if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) { var val = inputAxisInfo && inputAxisInfo.value; if (val == null && !isIllegalPoint) { val = axis.pointToData(point); } val != null && processOnAxis(axisInfo, val, updaters, false, outputFinder); } }); }); // Process for linked axes. var linkTriggers = {}; each$15(axesInfo, function (tarAxisInfo, tarKey) { var linkGroup = tarAxisInfo.linkGroup; // If axis has been triggered in the previous stage, it should not be triggered by link. if (linkGroup && !showValueMap[tarKey]) { each$15(linkGroup.axesInfo, function (srcAxisInfo, srcKey) { var srcValItem = showValueMap[srcKey]; // If srcValItem exist, source axis is triggered, so link to target axis. if (srcAxisInfo !== tarAxisInfo && srcValItem) { var val = srcValItem.value; linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper( val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo) ))); linkTriggers[tarAxisInfo.key] = val; } }); } }); each$15(linkTriggers, function (val, tarKey) { processOnAxis(axesInfo[tarKey], val, updaters, true, outputFinder); }); updateModelActually(showValueMap, axesInfo, outputFinder); dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction); dispatchHighDownActually(axesInfo, dispatchAction, api); return outputFinder; }; function processOnAxis(axisInfo, newValue, updaters, dontSnap, outputFinder) { var axis = axisInfo.axis; if (axis.scale.isBlank() || !axis.containData(newValue)) { return; } if (!axisInfo.involveSeries) { updaters.showPointer(axisInfo, newValue); return; } // Heavy calculation. So put it after axis.containData checking. var payloadInfo = buildPayloadsBySeries(newValue, axisInfo); var payloadBatch = payloadInfo.payloadBatch; var snapToValue = payloadInfo.snapToValue; // Fill content of event obj for echarts.connect. // By defualt use the first involved series data as a sample to connect. if (payloadBatch[0] && outputFinder.seriesIndex == null) { extend(outputFinder, payloadBatch[0]); } // If no linkSource input, this process is for collecting link // target, where snap should not be accepted. if (!dontSnap && axisInfo.snap) { if (axis.containData(snapToValue) && snapToValue != null) { newValue = snapToValue; } } updaters.showPointer(axisInfo, newValue, payloadBatch, outputFinder); // Tooltip should always be snapToValue, otherwise there will be // incorrect "axis value ~ series value" mapping displayed in tooltip. updaters.showTooltip(axisInfo, payloadInfo, snapToValue); } function buildPayloadsBySeries(value, axisInfo) { var axis = axisInfo.axis; var dim = axis.dim; var snapToValue = value; var payloadBatch = []; var minDist = Number.MAX_VALUE; var minDiff = -1; each$15(axisInfo.seriesModels, function (series, idx) { var dataDim = series.getData().mapDimension(dim, true); var seriesNestestValue; var dataIndices; if (series.getAxisTooltipData) { var result = series.getAxisTooltipData(dataDim, value, axis); dataIndices = result.dataIndices; seriesNestestValue = result.nestestValue; } else { dataIndices = series.getData().indicesOfNearest( dataDim[0], value, // Add a threshold to avoid find the wrong dataIndex // when data length is not same. // false, axis.type === 'category' ? 0.5 : null ); if (!dataIndices.length) { return; } seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]); } if (seriesNestestValue == null || !isFinite(seriesNestestValue)) { return; } var diff = value - seriesNestestValue; var dist = Math.abs(diff); // Consider category case if (dist <= minDist) { if (dist < minDist || (diff >= 0 && minDiff < 0)) { minDist = dist; minDiff = diff; snapToValue = seriesNestestValue; payloadBatch.length = 0; } each$15(dataIndices, function (dataIndex) { payloadBatch.push({ seriesIndex: series.seriesIndex, dataIndexInside: dataIndex, dataIndex: series.getData().getRawIndex(dataIndex) }); }); } }); return { payloadBatch: payloadBatch, snapToValue: snapToValue }; } function showPointer(showValueMap, axisInfo, value, payloadBatch) { showValueMap[axisInfo.key] = {value: value, payloadBatch: payloadBatch}; } function showTooltip(dataByCoordSys, axisInfo, payloadInfo, value) { var payloadBatch = payloadInfo.payloadBatch; var axis = axisInfo.axis; var axisModel = axis.model; var axisPointerModel = axisInfo.axisPointerModel; // If no data, do not create anything in dataByCoordSys, // whose length will be used to judge whether dispatch action. if (!axisInfo.triggerTooltip || !payloadBatch.length) { return; } var coordSysModel = axisInfo.coordSys.model; var coordSysKey = makeKey(coordSysModel); var coordSysItem = dataByCoordSys.map[coordSysKey]; if (!coordSysItem) { coordSysItem = dataByCoordSys.map[coordSysKey] = { coordSysId: coordSysModel.id, coordSysIndex: coordSysModel.componentIndex, coordSysType: coordSysModel.type, coordSysMainType: coordSysModel.mainType, dataByAxis: [] }; dataByCoordSys.list.push(coordSysItem); } coordSysItem.dataByAxis.push({ axisDim: axis.dim, axisIndex: axisModel.componentIndex, axisType: axisModel.type, axisId: axisModel.id, value: value, // Caustion: viewHelper.getValueLabel is actually on "view stage", which // depends that all models have been updated. So it should not be performed // here. Considering axisPointerModel used here is volatile, which is hard // to be retrieve in TooltipView, we prepare parameters here. valueLabelOpt: { precision: axisPointerModel.get('label.precision'), formatter: axisPointerModel.get('label.formatter') }, seriesDataIndices: payloadBatch.slice() }); } function updateModelActually(showValueMap, axesInfo, outputFinder) { var outputAxesInfo = outputFinder.axesInfo = []; // Basic logic: If no 'show' required, 'hide' this axisPointer. each$15(axesInfo, function (axisInfo, key) { var option = axisInfo.axisPointerModel.option; var valItem = showValueMap[key]; if (valItem) { !axisInfo.useHandle && (option.status = 'show'); option.value = valItem.value; // For label formatter param and highlight. option.seriesDataIndices = (valItem.payloadBatch || []).slice(); } // When always show (e.g., handle used), remain // original value and status. else { // If hide, value still need to be set, consider // click legend to toggle axis blank. !axisInfo.useHandle && (option.status = 'hide'); } // If status is 'hide', should be no info in payload. option.status === 'show' && outputAxesInfo.push({ axisDim: axisInfo.axis.dim, axisIndex: axisInfo.axis.model.componentIndex, value: option.value }); }); } function dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction) { // Basic logic: If no showTip required, hideTip will be dispatched. if (illegalPoint(point) || !dataByCoordSys.list.length) { dispatchAction({type: 'hideTip'}); return; } // In most case only one axis (or event one series is used). It is // convinient to fetch payload.seriesIndex and payload.dataIndex // dirtectly. So put the first seriesIndex and dataIndex of the first // axis on the payload. var sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {}; dispatchAction({ type: 'showTip', escapeConnect: true, x: point[0], y: point[1], tooltipOption: payload.tooltipOption, position: payload.position, dataIndexInside: sampleItem.dataIndexInside, dataIndex: sampleItem.dataIndex, seriesIndex: sampleItem.seriesIndex, dataByCoordSys: dataByCoordSys.list }); } function dispatchHighDownActually(axesInfo, dispatchAction, api) { // FIXME // highlight status modification shoule be a stage of main process? // (Consider confilct (e.g., legend and axisPointer) and setOption) var zr = api.getZr(); var highDownKey = 'axisPointerLastHighlights'; var lastHighlights = inner$6(zr)[highDownKey] || {}; var newHighlights = inner$6(zr)[highDownKey] = {}; // Update highlight/downplay status according to axisPointer model. // Build hash map and remove duplicate incidentally. each$15(axesInfo, function (axisInfo, key) { var option = axisInfo.axisPointerModel.option; option.status === 'show' && each$15(option.seriesDataIndices, function (batchItem) { var key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex; newHighlights[key] = batchItem; }); }); // Diff. var toHighlight = []; var toDownplay = []; each$1(lastHighlights, function (batchItem, key) { !newHighlights[key] && toDownplay.push(batchItem); }); each$1(newHighlights, function (batchItem, key) { !lastHighlights[key] && toHighlight.push(batchItem); }); toDownplay.length && api.dispatchAction({ type: 'downplay', escapeConnect: true, batch: toDownplay }); toHighlight.length && api.dispatchAction({ type: 'highlight', escapeConnect: true, batch: toHighlight }); } function findInputAxisInfo(inputAxesInfo, axisInfo) { for (var i = 0; i < (inputAxesInfo || []).length; i++) { var inputAxisInfo = inputAxesInfo[i]; if (axisInfo.axis.dim === inputAxisInfo.axisDim && axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex ) { return inputAxisInfo; } } } function makeMapperParam(axisInfo) { var axisModel = axisInfo.axis.model; var item = {}; var dim = item.axisDim = axisInfo.axis.dim; item.axisIndex = item[dim + 'AxisIndex'] = axisModel.componentIndex; item.axisName = item[dim + 'AxisName'] = axisModel.name; item.axisId = item[dim + 'AxisId'] = axisModel.id; return item; } function illegalPoint(point) { return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]); } var AxisPointerModel = extendComponentModel({ type: 'axisPointer', coordSysAxesInfo: null, defaultOption: { // 'auto' means that show when triggered by tooltip or handle. show: 'auto', // 'click' | 'mousemove' | 'none' triggerOn: null, // set default in AxisPonterView.js zlevel: 0, z: 50, type: 'line', // axispointer triggered by tootip determine snap automatically, // see `modelHelper`. snap: false, triggerTooltip: true, value: null, status: null, // Init value depends on whether handle is used. // [group0, group1, ...] // Each group can be: { // mapper: function () {}, // singleTooltip: 'multiple', // 'multiple' or 'single' // xAxisId: ..., // yAxisName: ..., // angleAxisIndex: ... // } // mapper: can be ignored. // input: {axisInfo, value} // output: {axisInfo, value} link: [], // Do not set 'auto' here, otherwise global animation: false // will not effect at this axispointer. animation: null, animationDurationUpdate: 200, lineStyle: { color: '#aaa', width: 1, type: 'solid' }, shadowStyle: { color: 'rgba(150,150,150,0.3)' }, label: { show: true, formatter: null, // string | Function precision: 'auto', // Or a number like 0, 1, 2 ... margin: 3, color: '#fff', padding: [5, 7, 5, 7], backgroundColor: 'auto', // default: axis line color borderColor: null, borderWidth: 0, shadowBlur: 3, shadowColor: '#aaa' // Considering applicability, common style should // better not have shadowOffset. // shadowOffsetX: 0, // shadowOffsetY: 2 }, handle: { show: false, icon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z', // jshint ignore:line size: 45, // handle margin is from symbol center to axis, which is stable when circular move. margin: 50, // color: '#1b8bbd' // color: '#2f4554' color: '#333', shadowBlur: 3, shadowColor: '#aaa', shadowOffsetX: 0, shadowOffsetY: 2, // For mobile performance throttle: 40 } } }); var inner$7 = makeInner(); var each$16 = each$1; /** * @param {string} key * @param {module:echarts/ExtensionAPI} api * @param {Function} handler * param: {string} currTrigger * param: {Array.<number>} point */ function register(key, api, handler) { if (env$1.node) { return; } var zr = api.getZr(); inner$7(zr).records || (inner$7(zr).records = {}); initGlobalListeners(zr, api); var record = inner$7(zr).records[key] || (inner$7(zr).records[key] = {}); record.handler = handler; } function initGlobalListeners(zr, api) { if (inner$7(zr).initialized) { return; } inner$7(zr).initialized = true; useHandler('click', curry(doEnter, 'click')); useHandler('mousemove', curry(doEnter, 'mousemove')); // useHandler('mouseout', onLeave); useHandler('globalout', onLeave); function useHandler(eventType, cb) { zr.on(eventType, function (e) { var dis = makeDispatchAction(api); each$16(inner$7(zr).records, function (record) { record && cb(record, e, dis.dispatchAction); }); dispatchTooltipFinally(dis.pendings, api); }); } } function dispatchTooltipFinally(pendings, api) { var showLen = pendings.showTip.length; var hideLen = pendings.hideTip.length; var actuallyPayload; if (showLen) { actuallyPayload = pendings.showTip[showLen - 1]; } else if (hideLen) { actuallyPayload = pendings.hideTip[hideLen - 1]; } if (actuallyPayload) { actuallyPayload.dispatchAction = null; api.dispatchAction(actuallyPayload); } } function onLeave(record, e, dispatchAction) { record.handler('leave', null, dispatchAction); } function doEnter(currTrigger, record, e, dispatchAction) { record.handler(currTrigger, e, dispatchAction); } function makeDispatchAction(api) { var pendings = { showTip: [], hideTip: [] }; // FIXME // better approach? // 'showTip' and 'hideTip' can be triggered by axisPointer and tooltip, // which may be conflict, (axisPointer call showTip but tooltip call hideTip); // So we have to add "final stage" to merge those dispatched actions. var dispatchAction = function (payload) { var pendingList = pendings[payload.type]; if (pendingList) { pendingList.push(payload); } else { payload.dispatchAction = dispatchAction; api.dispatchAction(payload); } }; return { dispatchAction: dispatchAction, pendings: pendings }; } /** * @param {string} key * @param {module:echarts/ExtensionAPI} api */ function unregister(key, api) { if (env$1.node) { return; } var zr = api.getZr(); var record = (inner$7(zr).records || {})[key]; if (record) { inner$7(zr).records[key] = null; } } var AxisPointerView = extendComponentView({ type: 'axisPointer', render: function (globalAxisPointerModel, ecModel, api) { var globalTooltipModel = ecModel.getComponent('tooltip'); var triggerOn = globalAxisPointerModel.get('triggerOn') || (globalTooltipModel && globalTooltipModel.get('triggerOn') || 'mousemove|click'); // Register global listener in AxisPointerView to enable // AxisPointerView to be independent to Tooltip. register( 'axisPointer', api, function (currTrigger, e, dispatchAction) { // If 'none', it is not controlled by mouse totally. if (triggerOn !== 'none' && (currTrigger === 'leave' || triggerOn.indexOf(currTrigger) >= 0) ) { dispatchAction({ type: 'updateAxisPointer', currTrigger: currTrigger, x: e && e.offsetX, y: e && e.offsetY }); } } ); }, /** * @override */ remove: function (ecModel, api) { unregister(api.getZr(), 'axisPointer'); AxisPointerView.superApply(this._model, 'remove', arguments); }, /** * @override */ dispose: function (ecModel, api) { unregister('axisPointer', api); AxisPointerView.superApply(this._model, 'dispose', arguments); } }); var inner$8 = makeInner(); var clone$4 = clone; var bind$2 = bind; /** * Base axis pointer class in 2D. * Implemenents {module:echarts/component/axis/IAxisPointer}. */ function BaseAxisPointer () { } BaseAxisPointer.prototype = { /** * @private */ _group: null, /** * @private */ _lastGraphicKey: null, /** * @private */ _handle: null, /** * @private */ _dragging: false, /** * @private */ _lastValue: null, /** * @private */ _lastStatus: null, /** * @private */ _payloadInfo: null, /** * In px, arbitrary value. Do not set too small, * no animation is ok for most cases. * @protected */ animationThreshold: 15, /** * @implement */ render: function (axisModel, axisPointerModel, api, forceRender) { var value = axisPointerModel.get('value'); var status = axisPointerModel.get('status'); // Bind them to `this`, not in closure, otherwise they will not // be replaced when user calling setOption in not merge mode. this._axisModel = axisModel; this._axisPointerModel = axisPointerModel; this._api = api; // Optimize: `render` will be called repeatly during mouse move. // So it is power consuming if performing `render` each time, // especially on mobile device. if (!forceRender && this._lastValue === value && this._lastStatus === status ) { return; } this._lastValue = value; this._lastStatus = status; var group = this._group; var handle = this._handle; if (!status || status === 'hide') { // Do not clear here, for animation better. group && group.hide(); handle && handle.hide(); return; } group && group.show(); handle && handle.show(); // Otherwise status is 'show' var elOption = {}; this.makeElOption(elOption, value, axisModel, axisPointerModel, api); // Enable change axis pointer type. var graphicKey = elOption.graphicKey; if (graphicKey !== this._lastGraphicKey) { this.clear(api); } this._lastGraphicKey = graphicKey; var moveAnimation = this._moveAnimation = this.determineAnimation(axisModel, axisPointerModel); if (!group) { group = this._group = new Group(); this.createPointerEl(group, elOption, axisModel, axisPointerModel); this.createLabelEl(group, elOption, axisModel, axisPointerModel); api.getZr().add(group); } else { var doUpdateProps = curry(updateProps$1, axisPointerModel, moveAnimation); this.updatePointerEl(group, elOption, doUpdateProps, axisPointerModel); this.updateLabelEl(group, elOption, doUpdateProps, axisPointerModel); } updateMandatoryProps(group, axisPointerModel, true); this._renderHandle(value); }, /** * @implement */ remove: function (api) { this.clear(api); }, /** * @implement */ dispose: function (api) { this.clear(api); }, /** * @protected */ determineAnimation: function (axisModel, axisPointerModel) { var animation = axisPointerModel.get('animation'); var axis = axisModel.axis; var isCategoryAxis = axis.type === 'category'; var useSnap = axisPointerModel.get('snap'); // Value axis without snap always do not snap. if (!useSnap && !isCategoryAxis) { return false; } if (animation === 'auto' || animation == null) { var animationThreshold = this.animationThreshold; if (isCategoryAxis && axis.getBandWidth() > animationThreshold) { return true; } // It is important to auto animation when snap used. Consider if there is // a dataZoom, animation will be disabled when too many points exist, while // it will be enabled for better visual effect when little points exist. if (useSnap) { var seriesDataCount = getAxisInfo(axisModel).seriesDataCount; var axisExtent = axis.getExtent(); // Approximate band width return Math.abs(axisExtent[0] - axisExtent[1]) / seriesDataCount > animationThreshold; } return false; } return animation === true; }, /** * add {pointer, label, graphicKey} to elOption * @protected */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { // Shoule be implemenented by sub-class. }, /** * @protected */ createPointerEl: function (group, elOption, axisModel, axisPointerModel) { var pointerOption = elOption.pointer; if (pointerOption) { var pointerEl = inner$8(group).pointerEl = new graphic[pointerOption.type]( clone$4(elOption.pointer) ); group.add(pointerEl); } }, /** * @protected */ createLabelEl: function (group, elOption, axisModel, axisPointerModel) { if (elOption.label) { var labelEl = inner$8(group).labelEl = new Rect( clone$4(elOption.label) ); group.add(labelEl); updateLabelShowHide(labelEl, axisPointerModel); } }, /** * @protected */ updatePointerEl: function (group, elOption, updateProps$$1) { var pointerEl = inner$8(group).pointerEl; if (pointerEl) { pointerEl.setStyle(elOption.pointer.style); updateProps$$1(pointerEl, {shape: elOption.pointer.shape}); } }, /** * @protected */ updateLabelEl: function (group, elOption, updateProps$$1, axisPointerModel) { var labelEl = inner$8(group).labelEl; if (labelEl) { labelEl.setStyle(elOption.label.style); updateProps$$1(labelEl, { // Consider text length change in vertical axis, animation should // be used on shape, otherwise the effect will be weird. shape: elOption.label.shape, position: elOption.label.position }); updateLabelShowHide(labelEl, axisPointerModel); } }, /** * @private */ _renderHandle: function (value) { if (this._dragging || !this.updateHandleTransform) { return; } var axisPointerModel = this._axisPointerModel; var zr = this._api.getZr(); var handle = this._handle; var handleModel = axisPointerModel.getModel('handle'); var status = axisPointerModel.get('status'); if (!handleModel.get('show') || !status || status === 'hide') { handle && zr.remove(handle); this._handle = null; return; } var isInit; if (!this._handle) { isInit = true; handle = this._handle = createIcon( handleModel.get('icon'), { cursor: 'move', draggable: true, onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, onmousedown: bind$2(this._onHandleDragMove, this, 0, 0), drift: bind$2(this._onHandleDragMove, this), ondragend: bind$2(this._onHandleDragEnd, this) } ); zr.add(handle); } updateMandatoryProps(handle, axisPointerModel, false); // update style var includeStyles = [ 'color', 'borderColor', 'borderWidth', 'opacity', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY' ]; handle.setStyle(handleModel.getItemStyle(null, includeStyles)); // update position var handleSize = handleModel.get('size'); if (!isArray(handleSize)) { handleSize = [handleSize, handleSize]; } handle.attr('scale', [handleSize[0] / 2, handleSize[1] / 2]); createOrUpdate( this, '_doDispatchAxisPointer', handleModel.get('throttle') || 0, 'fixRate' ); this._moveHandleToValue(value, isInit); }, /** * @private */ _moveHandleToValue: function (value, isInit) { updateProps$1( this._axisPointerModel, !isInit && this._moveAnimation, this._handle, getHandleTransProps(this.getHandleTransform( value, this._axisModel, this._axisPointerModel )) ); }, /** * @private */ _onHandleDragMove: function (dx, dy) { var handle = this._handle; if (!handle) { return; } this._dragging = true; // Persistent for throttle. var trans = this.updateHandleTransform( getHandleTransProps(handle), [dx, dy], this._axisModel, this._axisPointerModel ); this._payloadInfo = trans; handle.stopAnimation(); handle.attr(getHandleTransProps(trans)); inner$8(handle).lastProp = null; this._doDispatchAxisPointer(); }, /** * Throttled method. * @private */ _doDispatchAxisPointer: function () { var handle = this._handle; if (!handle) { return; } var payloadInfo = this._payloadInfo; var axisModel = this._axisModel; this._api.dispatchAction({ type: 'updateAxisPointer', x: payloadInfo.cursorPoint[0], y: payloadInfo.cursorPoint[1], tooltipOption: payloadInfo.tooltipOption, axesInfo: [{ axisDim: axisModel.axis.dim, axisIndex: axisModel.componentIndex }] }); }, /** * @private */ _onHandleDragEnd: function (moveAnimation) { this._dragging = false; var handle = this._handle; if (!handle) { return; } var value = this._axisPointerModel.get('value'); // Consider snap or categroy axis, handle may be not consistent with // axisPointer. So move handle to align the exact value position when // drag ended. this._moveHandleToValue(value); // For the effect: tooltip will be shown when finger holding on handle // button, and will be hidden after finger left handle button. this._api.dispatchAction({ type: 'hideTip' }); }, /** * Should be implemenented by sub-class if support `handle`. * @protected * @param {number} value * @param {module:echarts/model/Model} axisModel * @param {module:echarts/model/Model} axisPointerModel * @return {Object} {position: [x, y], rotation: 0} */ getHandleTransform: null, /** * * Should be implemenented by sub-class if support `handle`. * @protected * @param {Object} transform {position, rotation} * @param {Array.<number>} delta [dx, dy] * @param {module:echarts/model/Model} axisModel * @param {module:echarts/model/Model} axisPointerModel * @return {Object} {position: [x, y], rotation: 0, cursorPoint: [x, y]} */ updateHandleTransform: null, /** * @private */ clear: function (api) { this._lastValue = null; this._lastStatus = null; var zr = api.getZr(); var group = this._group; var handle = this._handle; if (zr && group) { this._lastGraphicKey = null; group && zr.remove(group); handle && zr.remove(handle); this._group = null; this._handle = null; this._payloadInfo = null; } }, /** * @protected */ doClear: function () { // Implemented by sub-class if necessary. }, /** * @protected * @param {Array.<number>} xy * @param {Array.<number>} wh * @param {number} [xDimIndex=0] or 1 */ buildLabel: function (xy, wh, xDimIndex) { xDimIndex = xDimIndex || 0; return { x: xy[xDimIndex], y: xy[1 - xDimIndex], width: wh[xDimIndex], height: wh[1 - xDimIndex] }; } }; BaseAxisPointer.prototype.constructor = BaseAxisPointer; function updateProps$1(animationModel, moveAnimation, el, props) { // Animation optimize. if (!propsEqual(inner$8(el).lastProp, props)) { inner$8(el).lastProp = props; moveAnimation ? updateProps(el, props, animationModel) : (el.stopAnimation(), el.attr(props)); } } function propsEqual(lastProps, newProps) { if (isObject$1(lastProps) && isObject$1(newProps)) { var equals = true; each$1(newProps, function (item, key) { equals = equals && propsEqual(lastProps[key], item); }); return !!equals; } else { return lastProps === newProps; } } function updateLabelShowHide(labelEl, axisPointerModel) { labelEl[axisPointerModel.get('label.show') ? 'show' : 'hide'](); } function getHandleTransProps(trans) { return { position: trans.position.slice(), rotation: trans.rotation || 0 }; } function updateMandatoryProps(group, axisPointerModel, silent) { var z = axisPointerModel.get('z'); var zlevel = axisPointerModel.get('zlevel'); group && group.traverse(function (el) { if (el.type !== 'group') { z != null && (el.z = z); zlevel != null && (el.zlevel = zlevel); el.silent = silent; } }); } enableClassExtend(BaseAxisPointer); /** * @param {module:echarts/model/Model} axisPointerModel */ function buildElStyle(axisPointerModel) { var axisPointerType = axisPointerModel.get('type'); var styleModel = axisPointerModel.getModel(axisPointerType + 'Style'); var style; if (axisPointerType === 'line') { style = styleModel.getLineStyle(); style.fill = null; } else if (axisPointerType === 'shadow') { style = styleModel.getAreaStyle(); style.stroke = null; } return style; } /** * @param {Function} labelPos {align, verticalAlign, position} */ function buildLabelElOption( elOption, axisModel, axisPointerModel, api, labelPos ) { var value = axisPointerModel.get('value'); var text = getValueLabel( value, axisModel.axis, axisModel.ecModel, axisPointerModel.get('seriesDataIndices'), { precision: axisPointerModel.get('label.precision'), formatter: axisPointerModel.get('label.formatter') } ); var labelModel = axisPointerModel.getModel('label'); var paddings = normalizeCssArray$1(labelModel.get('padding') || 0); var font = labelModel.getFont(); var textRect = getBoundingRect(text, font); var position = labelPos.position; var width = textRect.width + paddings[1] + paddings[3]; var height = textRect.height + paddings[0] + paddings[2]; // Adjust by align. var align = labelPos.align; align === 'right' && (position[0] -= width); align === 'center' && (position[0] -= width / 2); var verticalAlign = labelPos.verticalAlign; verticalAlign === 'bottom' && (position[1] -= height); verticalAlign === 'middle' && (position[1] -= height / 2); // Not overflow ec container confineInContainer(position, width, height, api); var bgColor = labelModel.get('backgroundColor'); if (!bgColor || bgColor === 'auto') { bgColor = axisModel.get('axisLine.lineStyle.color'); } elOption.label = { shape: {x: 0, y: 0, width: width, height: height, r: labelModel.get('borderRadius')}, position: position.slice(), // TODO: rich style: { text: text, textFont: font, textFill: labelModel.getTextColor(), textPosition: 'inside', fill: bgColor, stroke: labelModel.get('borderColor') || 'transparent', lineWidth: labelModel.get('borderWidth') || 0, shadowBlur: labelModel.get('shadowBlur'), shadowColor: labelModel.get('shadowColor'), shadowOffsetX: labelModel.get('shadowOffsetX'), shadowOffsetY: labelModel.get('shadowOffsetY') }, // Lable should be over axisPointer. z2: 10 }; } // Do not overflow ec container function confineInContainer(position, width, height, api) { var viewWidth = api.getWidth(); var viewHeight = api.getHeight(); position[0] = Math.min(position[0] + width, viewWidth) - width; position[1] = Math.min(position[1] + height, viewHeight) - height; position[0] = Math.max(position[0], 0); position[1] = Math.max(position[1], 0); } /** * @param {number} value * @param {module:echarts/coord/Axis} axis * @param {module:echarts/model/Global} ecModel * @param {Object} opt * @param {Array.<Object>} seriesDataIndices * @param {number|string} opt.precision 'auto' or a number * @param {string|Function} opt.formatter label formatter */ function getValueLabel(value, axis, ecModel, seriesDataIndices, opt) { var text = axis.scale.getLabel( // If `precision` is set, width can be fixed (like '12.00500'), which // helps to debounce when when moving label. value, {precision: opt.precision} ); var formatter = opt.formatter; if (formatter) { var params = { value: getAxisRawValue(axis, value), seriesData: [] }; each$1(seriesDataIndices, function (idxItem) { var series = ecModel.getSeriesByIndex(idxItem.seriesIndex); var dataIndex = idxItem.dataIndexInside; var dataParams = series && series.getDataParams(dataIndex); dataParams && params.seriesData.push(dataParams); }); if (isString(formatter)) { text = formatter.replace('{value}', text); } else if (isFunction$1(formatter)) { text = formatter(params); } } return text; } /** * @param {module:echarts/coord/Axis} axis * @param {number} value * @param {Object} layoutInfo { * rotation, position, labelOffset, labelDirection, labelMargin * } */ function getTransformedPosition (axis, value, layoutInfo) { var transform = create$1(); rotate(transform, transform, layoutInfo.rotation); translate(transform, transform, layoutInfo.position); return applyTransform$1([ axis.dataToCoord(value), (layoutInfo.labelOffset || 0) + (layoutInfo.labelDirection || 1) * (layoutInfo.labelMargin || 0) ], transform); } function buildCartesianSingleLabelElOption( value, elOption, layoutInfo, axisModel, axisPointerModel, api ) { var textLayout = AxisBuilder.innerTextLayout( layoutInfo.rotation, 0, layoutInfo.labelDirection ); layoutInfo.labelMargin = axisPointerModel.get('label.margin'); buildLabelElOption(elOption, axisModel, axisPointerModel, api, { position: getTransformedPosition(axisModel.axis, value, layoutInfo), align: textLayout.textAlign, verticalAlign: textLayout.textVerticalAlign }); } /** * @param {Array.<number>} p1 * @param {Array.<number>} p2 * @param {number} [xDimIndex=0] or 1 */ function makeLineShape(p1, p2, xDimIndex) { xDimIndex = xDimIndex || 0; return { x1: p1[xDimIndex], y1: p1[1 - xDimIndex], x2: p2[xDimIndex], y2: p2[1 - xDimIndex] }; } /** * @param {Array.<number>} xy * @param {Array.<number>} wh * @param {number} [xDimIndex=0] or 1 */ function makeRectShape(xy, wh, xDimIndex) { xDimIndex = xDimIndex || 0; return { x: xy[xDimIndex], y: xy[1 - xDimIndex], width: wh[xDimIndex], height: wh[1 - xDimIndex] }; } function makeSectorShape(cx, cy, r0, r, startAngle, endAngle) { return { cx: cx, cy: cy, r0: r0, r: r, startAngle: startAngle, endAngle: endAngle, clockwise: true }; } var CartesianAxisPointer = BaseAxisPointer.extend({ /** * @override */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { var axis = axisModel.axis; var grid = axis.grid; var axisPointerType = axisPointerModel.get('type'); var otherExtent = getCartesian(grid, axis).getOtherAxis(axis).getGlobalExtent(); var pixelValue = axis.toGlobalCoord(axis.dataToCoord(value, true)); if (axisPointerType && axisPointerType !== 'none') { var elStyle = buildElStyle(axisPointerModel); var pointerOption = pointerShapeBuilder[axisPointerType]( axis, pixelValue, otherExtent, elStyle ); pointerOption.style = elStyle; elOption.graphicKey = pointerOption.type; elOption.pointer = pointerOption; } var layoutInfo = layout$1(grid.model, axisModel); buildCartesianSingleLabelElOption( value, elOption, layoutInfo, axisModel, axisPointerModel, api ); }, /** * @override */ getHandleTransform: function (value, axisModel, axisPointerModel) { var layoutInfo = layout$1(axisModel.axis.grid.model, axisModel, { labelInside: false }); layoutInfo.labelMargin = axisPointerModel.get('handle.margin'); return { position: getTransformedPosition(axisModel.axis, value, layoutInfo), rotation: layoutInfo.rotation + (layoutInfo.labelDirection < 0 ? Math.PI : 0) }; }, /** * @override */ updateHandleTransform: function (transform, delta, axisModel, axisPointerModel) { var axis = axisModel.axis; var grid = axis.grid; var axisExtent = axis.getGlobalExtent(true); var otherExtent = getCartesian(grid, axis).getOtherAxis(axis).getGlobalExtent(); var dimIndex = axis.dim === 'x' ? 0 : 1; var currPosition = transform.position; currPosition[dimIndex] += delta[dimIndex]; currPosition[dimIndex] = Math.min(axisExtent[1], currPosition[dimIndex]); currPosition[dimIndex] = Math.max(axisExtent[0], currPosition[dimIndex]); var cursorOtherValue = (otherExtent[1] + otherExtent[0]) / 2; var cursorPoint = [cursorOtherValue, cursorOtherValue]; cursorPoint[dimIndex] = currPosition[dimIndex]; // Make tooltip do not overlap axisPointer and in the middle of the grid. var tooltipOptions = [{verticalAlign: 'middle'}, {align: 'center'}]; return { position: currPosition, rotation: transform.rotation, cursorPoint: cursorPoint, tooltipOption: tooltipOptions[dimIndex] }; } }); function getCartesian(grid, axis) { var opt = {}; opt[axis.dim + 'AxisIndex'] = axis.index; return grid.getCartesian(opt); } var pointerShapeBuilder = { line: function (axis, pixelValue, otherExtent, elStyle) { var targetShape = makeLineShape( [pixelValue, otherExtent[0]], [pixelValue, otherExtent[1]], getAxisDimIndex(axis) ); subPixelOptimizeLine({ shape: targetShape, style: elStyle }); return { type: 'Line', shape: targetShape }; }, shadow: function (axis, pixelValue, otherExtent, elStyle) { var bandWidth = axis.getBandWidth(); var span = otherExtent[1] - otherExtent[0]; return { type: 'Rect', shape: makeRectShape( [pixelValue - bandWidth / 2, otherExtent[0]], [bandWidth, span], getAxisDimIndex(axis) ) }; } }; function getAxisDimIndex(axis) { return axis.dim === 'x' ? 0 : 1; } AxisView.registerAxisPointerClass('CartesianAxisPointer', CartesianAxisPointer); // CartesianAxisPointer is not supposed to be required here. But consider // echarts.simple.js and online build tooltip, which only require gridSimple, // CartesianAxisPointer should be able to required somewhere. registerPreprocessor(function (option) { // Always has a global axisPointerModel for default setting. if (option) { (!option.axisPointer || option.axisPointer.length === 0) && (option.axisPointer = {}); var link = option.axisPointer.link; // Normalize to array to avoid object mergin. But if link // is not set, remain null/undefined, otherwise it will // override existent link setting. if (link && !isArray(link)) { option.axisPointer.link = [link]; } } }); // This process should proformed after coordinate systems created // and series data processed. So put it on statistic processing stage. registerProcessor(PRIORITY.PROCESSOR.STATISTIC, function (ecModel, api) { // Build axisPointerModel, mergin tooltip.axisPointer model for each axis. // allAxesInfo should be updated when setOption performed. ecModel.getComponent('axisPointer').coordSysAxesInfo = collect(ecModel, api); }); // Broadcast to all views. registerAction({ type: 'updateAxisPointer', event: 'updateAxisPointer', update: ':updateAxisPointer' }, axisTrigger); var XY = ['x', 'y']; var WH = ['width', 'height']; var SingleAxisPointer = BaseAxisPointer.extend({ /** * @override */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { var axis = axisModel.axis; var coordSys = axis.coordinateSystem; var otherExtent = getGlobalExtent(coordSys, 1 - getPointDimIndex(axis)); var pixelValue = coordSys.dataToPoint(value)[0]; var axisPointerType = axisPointerModel.get('type'); if (axisPointerType && axisPointerType !== 'none') { var elStyle = buildElStyle(axisPointerModel); var pointerOption = pointerShapeBuilder$1[axisPointerType]( axis, pixelValue, otherExtent, elStyle ); pointerOption.style = elStyle; elOption.graphicKey = pointerOption.type; elOption.pointer = pointerOption; } var layoutInfo = layout$2(axisModel); buildCartesianSingleLabelElOption( value, elOption, layoutInfo, axisModel, axisPointerModel, api ); }, /** * @override */ getHandleTransform: function (value, axisModel, axisPointerModel) { var layoutInfo = layout$2(axisModel, {labelInside: false}); layoutInfo.labelMargin = axisPointerModel.get('handle.margin'); return { position: getTransformedPosition(axisModel.axis, value, layoutInfo), rotation: layoutInfo.rotation + (layoutInfo.labelDirection < 0 ? Math.PI : 0) }; }, /** * @override */ updateHandleTransform: function (transform, delta, axisModel, axisPointerModel) { var axis = axisModel.axis; var coordSys = axis.coordinateSystem; var dimIndex = getPointDimIndex(axis); var axisExtent = getGlobalExtent(coordSys, dimIndex); var currPosition = transform.position; currPosition[dimIndex] += delta[dimIndex]; currPosition[dimIndex] = Math.min(axisExtent[1], currPosition[dimIndex]); currPosition[dimIndex] = Math.max(axisExtent[0], currPosition[dimIndex]); var otherExtent = getGlobalExtent(coordSys, 1 - dimIndex); var cursorOtherValue = (otherExtent[1] + otherExtent[0]) / 2; var cursorPoint = [cursorOtherValue, cursorOtherValue]; cursorPoint[dimIndex] = currPosition[dimIndex]; return { position: currPosition, rotation: transform.rotation, cursorPoint: cursorPoint, tooltipOption: { verticalAlign: 'middle' } }; } }); var pointerShapeBuilder$1 = { line: function (axis, pixelValue, otherExtent, elStyle) { var targetShape = makeLineShape( [pixelValue, otherExtent[0]], [pixelValue, otherExtent[1]], getPointDimIndex(axis) ); subPixelOptimizeLine({ shape: targetShape, style: elStyle }); return { type: 'Line', shape: targetShape }; }, shadow: function (axis, pixelValue, otherExtent, elStyle) { var bandWidth = axis.getBandWidth(); var span = otherExtent[1] - otherExtent[0]; return { type: 'Rect', shape: makeRectShape( [pixelValue - bandWidth / 2, otherExtent[0]], [bandWidth, span], getPointDimIndex(axis) ) }; } }; function getPointDimIndex(axis) { return axis.isHorizontal() ? 0 : 1; } function getGlobalExtent(coordSys, dimIndex) { var rect = coordSys.getRect(); return [rect[XY[dimIndex]], rect[XY[dimIndex]] + rect[WH[dimIndex]]]; } AxisView.registerAxisPointerClass('SingleAxisPointer', SingleAxisPointer); extendComponentView({ type: 'single' }); /** * @file Define the themeRiver view's series model * @author Deqing Li(annong035@gmail.com) */ var DATA_NAME_INDEX = 2; var ThemeRiverSeries = SeriesModel.extend({ type: 'series.themeRiver', dependencies: ['singleAxis'], /** * @readOnly * @type {module:zrender/core/util#HashMap} */ nameMap: null, /** * @override */ init: function (option) { ThemeRiverSeries.superApply(this, 'init', arguments); // Put this function here is for the sake of consistency of code style. // Enable legend selection for each data item // Use a function instead of direct access because data reference may changed this.legendDataProvider = function () { return this.getRawData(); }; }, /** * If there is no value of a certain point in the time for some event,set it value to 0. * * @param {Array} data initial data in the option * @return {Array} */ fixData: function (data) { var rawDataLength = data.length; // grouped data by name var dataByName = nest() .key(function (dataItem) { return dataItem[2]; }) .entries(data); // data group in each layer var layData = map(dataByName, function (d) { return { name: d.key, dataList: d.values }; }); var layerNum = layData.length; var largestLayer = -1; var index = -1; for (var i = 0; i < layerNum; ++i) { var len = layData[i].dataList.length; if (len > largestLayer) { largestLayer = len; index = i; } } for (var k = 0; k < layerNum; ++k) { if (k === index) { continue; } var name = layData[k].name; for (var j = 0; j < largestLayer; ++j) { var timeValue = layData[index].dataList[j][0]; var length = layData[k].dataList.length; var keyIndex = -1; for (var l = 0; l < length; ++l) { var value = layData[k].dataList[l][0]; if (value === timeValue) { keyIndex = l; break; } } if (keyIndex === -1) { data[rawDataLength] = []; data[rawDataLength][0] = timeValue; data[rawDataLength][1] = 0; data[rawDataLength][2] = name; rawDataLength++; } } } return data; }, /** * @override * @param {Object} option the initial option that user gived * @param {module:echarts/model/Model} ecModel the model object for themeRiver option * @return {module:echarts/data/List} */ getInitialData: function (option, ecModel) { var singleAxisModel = ecModel.queryComponents({ mainType: 'singleAxis', index: this.get('singleAxisIndex'), id: this.get('singleAxisId') })[0]; var axisType = singleAxisModel.get('type'); // filter the data item with the value of label is undefined var filterData = filter(option.data, function (dataItem) { return dataItem[2] !== undefined; }); // ??? TODO design a stage to transfer data for themeRiver and lines? var data = this.fixData(filterData || []); var nameList = []; var nameMap = this.nameMap = createHashMap(); var count = 0; for (var i = 0; i < data.length; ++i) { nameList.push(data[i][DATA_NAME_INDEX]); if (!nameMap.get(data[i][DATA_NAME_INDEX])) { nameMap.set(data[i][DATA_NAME_INDEX], count); count++; } } var dimensionsInfo = createDimensions(data, { coordDimensions: ['single'], dimensionsDefine: [ { name: 'time', type: getDimensionTypeByAxis(axisType) }, { name: 'value', type: 'float' }, { name: 'name', type: 'ordinal' } ], encodeDefine: { single: 0, value: 1, itemName: 2 } }); var list = new List(dimensionsInfo, this); list.initData(data); return list; }, /** * The raw data is divided into multiple layers and each layer * has same name. * * @return {Array.<Array.<number>>} */ getLayerSeries: function () { var data = this.getData(); var lenCount = data.count(); var indexArr = []; for (var i = 0; i < lenCount; ++i) { indexArr[i] = i; } // data group by name var dataByName = nest() .key(function (index) { return data.get('name', index); }) .entries(indexArr); var layerSeries = map(dataByName, function (d) { return { name: d.key, indices: d.values }; }); var timeDim = data.mapDimension('single'); for (var j = 0; j < layerSeries.length; ++j) { layerSeries[j].indices.sort(comparer); } function comparer(index1, index2) { return data.get(timeDim, index1) - data.get(timeDim, index2); } return layerSeries; }, /** * Get data indices for show tooltip content * * @param {Array.<string>|string} dim single coordinate dimension * @param {number} value axis value * @param {module:echarts/coord/single/SingleAxis} baseAxis single Axis used * the themeRiver. * @return {Object} {dataIndices, nestestValue} */ getAxisTooltipData: function (dim, value, baseAxis) { if (!isArray(dim)) { dim = dim ? [dim] : []; } var data = this.getData(); var layerSeries = this.getLayerSeries(); var indices = []; var layerNum = layerSeries.length; var nestestValue; for (var i = 0; i < layerNum; ++i) { var minDist = Number.MAX_VALUE; var nearestIdx = -1; var pointNum = layerSeries[i].indices.length; for (var j = 0; j < pointNum; ++j) { var theValue = data.get(dim[0], layerSeries[i].indices[j]); var dist = Math.abs(theValue - value); if (dist <= minDist) { nestestValue = theValue; minDist = dist; nearestIdx = layerSeries[i].indices[j]; } } indices.push(nearestIdx); } return {dataIndices: indices, nestestValue: nestestValue}; }, /** * @override * @param {number} dataIndex index of data */ formatTooltip: function (dataIndex) { var data = this.getData(); var htmlName = data.getName(dataIndex); var htmlValue = data.get(data.mapDimension('value'), dataIndex); if (isNaN(htmlValue) || htmlValue == null) { htmlValue = '-'; } return encodeHTML(htmlName + ' : ' + htmlValue); }, defaultOption: { zlevel: 0, z: 2, coordinateSystem: 'singleAxis', // gap in axis's orthogonal orientation boundaryGap: ['10%', '10%'], // legendHoverLink: true, singleAxisIndex: 0, animationEasing: 'linear', label: { margin: 4, textAlign: 'right', show: true, position: 'left', color: '#000', fontSize: 11 }, emphasis: { label: { show: true } } } }); /** * @file The file used to draw themeRiver view * @author Deqing Li(annong035@gmail.com) */ extendChartView({ type: 'themeRiver', init: function () { this._layers = []; }, render: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var group = this.group; var layerSeries = seriesModel.getLayerSeries(); var layoutInfo = data.getLayout('layoutInfo'); var rect = layoutInfo.rect; var boundaryGap = layoutInfo.boundaryGap; group.attr('position', [0, rect.y + boundaryGap[0]]); function keyGetter(item) { return item.name; } var dataDiffer = new DataDiffer( this._layersSeries || [], layerSeries, keyGetter, keyGetter ); var newLayersGroups = {}; dataDiffer .add(bind(process, this, 'add')) .update(bind(process, this, 'update')) .remove(bind(process, this, 'remove')) .execute(); function process(status, idx, oldIdx) { var oldLayersGroups = this._layers; if (status === 'remove') { group.remove(oldLayersGroups[idx]); return; } var points0 = []; var points1 = []; var color; var indices = layerSeries[idx].indices; for (var j = 0; j < indices.length; j++) { var layout = data.getItemLayout(indices[j]); var x = layout.x; var y0 = layout.y0; var y = layout.y; points0.push([x, y0]); points1.push([x, y0 + y]); color = data.getItemVisual(indices[j], 'color'); } var polygon; var text; var textLayout = data.getItemLayout(indices[0]); var itemModel = data.getItemModel(indices[j - 1]); var labelModel = itemModel.getModel('label'); var margin = labelModel.get('margin'); if (status === 'add') { var layerGroup = newLayersGroups[idx] = new Group(); polygon = new Polygon$1({ shape: { points: points0, stackedOnPoints: points1, smooth: 0.4, stackedOnSmooth: 0.4, smoothConstraint: false }, z2: 0 }); text = new Text({ style: { x: textLayout.x - margin, y: textLayout.y0 + textLayout.y / 2 } }); layerGroup.add(polygon); layerGroup.add(text); group.add(layerGroup); polygon.setClipPath(createGridClipShape$3(polygon.getBoundingRect(), seriesModel, function () { polygon.removeClipPath(); })); } else { var layerGroup = oldLayersGroups[oldIdx]; polygon = layerGroup.childAt(0); text = layerGroup.childAt(1); group.add(layerGroup); newLayersGroups[idx] = layerGroup; updateProps(polygon, { shape: { points: points0, stackedOnPoints: points1 } }, seriesModel); updateProps(text, { style: { x: textLayout.x - margin, y: textLayout.y0 + textLayout.y / 2 } }, seriesModel); } var hoverItemStyleModel = itemModel.getModel('emphasis.itemStyle'); var itemStyleModel = itemModel.getModel('itemStyle'); setTextStyle(text.style, labelModel, { text: labelModel.get('show') ? seriesModel.getFormattedLabel(indices[j - 1], 'normal') || data.getName(indices[j - 1]) : null, textVerticalAlign: 'middle' }); polygon.setStyle(extend({ fill: color }, itemStyleModel.getItemStyle(['color']))); setHoverStyle(polygon, hoverItemStyleModel.getItemStyle()); } this._layersSeries = layerSeries; this._layers = newLayersGroups; }, dispose: function () {} }); // add animation to the view function createGridClipShape$3(rect, seriesModel, cb) { var rectEl = new Rect({ shape: { x: rect.x - 10, y: rect.y - 10, width: 0, height: rect.height + 20 } }); initProps(rectEl, { shape: { width: rect.width + 20, height: rect.height + 20 } }, seriesModel, cb); return rectEl; } /** * @file Using layout algorithm transform the raw data to layout information. * @author Deqing Li(annong035@gmail.com) */ var themeRiverLayout = function (ecModel, api) { ecModel.eachSeriesByType('themeRiver', function (seriesModel) { var data = seriesModel.getData(); var single = seriesModel.coordinateSystem; var layoutInfo = {}; // use the axis boundingRect for view var rect = single.getRect(); layoutInfo.rect = rect; var boundaryGap = seriesModel.get('boundaryGap'); var axis = single.getAxis(); layoutInfo.boundaryGap = boundaryGap; if (axis.orient === 'horizontal') { boundaryGap[0] = parsePercent$1(boundaryGap[0], rect.height); boundaryGap[1] = parsePercent$1(boundaryGap[1], rect.height); var height = rect.height - boundaryGap[0] - boundaryGap[1]; themeRiverLayout$1(data, seriesModel, height); } else { boundaryGap[0] = parsePercent$1(boundaryGap[0], rect.width); boundaryGap[1] = parsePercent$1(boundaryGap[1], rect.width); var width = rect.width - boundaryGap[0] - boundaryGap[1]; themeRiverLayout$1(data, seriesModel, width); } data.setLayout('layoutInfo', layoutInfo); }); }; /** * The layout information about themeriver * * @param {module:echarts/data/List} data data in the series * @param {module:echarts/model/Series} seriesModel the model object of themeRiver series * @param {number} height value used to compute every series height */ function themeRiverLayout$1(data, seriesModel, height) { if (!data.count()) { return; } var coordSys = seriesModel.coordinateSystem; // the data in each layer are organized into a series. var layerSeries = seriesModel.getLayerSeries(); // the points in each layer. var timeDim = data.mapDimension('single'); var valueDim = data.mapDimension('value'); var layerPoints = map(layerSeries, function (singleLayer) { return map(singleLayer.indices, function (idx) { var pt = coordSys.dataToPoint(data.get(timeDim, idx)); pt[1] = data.get(valueDim, idx); return pt; }); }); var base = computeBaseline(layerPoints); var baseLine = base.y0; var ky = height / base.max; // set layout information for each item. var n = layerSeries.length; var m = layerSeries[0].indices.length; var baseY0; for (var j = 0; j < m; ++j) { baseY0 = baseLine[j] * ky; data.setItemLayout(layerSeries[0].indices[j], { layerIndex: 0, x: layerPoints[0][j][0], y0: baseY0, y: layerPoints[0][j][1] * ky }); for (var i = 1; i < n; ++i) { baseY0 += layerPoints[i - 1][j][1] * ky; data.setItemLayout(layerSeries[i].indices[j], { layerIndex: i, x: layerPoints[i][j][0], y0: baseY0, y: layerPoints[i][j][1] * ky }); } } } /** * Compute the baseLine of the rawdata * Inspired by Lee Byron's paper Stacked Graphs - Geometry & Aesthetics * * @param {Array.<Array>} data the points in each layer * @return {Object} */ function computeBaseline(data) { var layerNum = data.length; var pointNum = data[0].length; var sums = []; var y0 = []; var max = 0; var temp; var base = {}; for (var i = 0; i < pointNum; ++i) { for (var j = 0, temp = 0; j < layerNum; ++j) { temp += data[j][i][1]; } if (temp > max) { max = temp; } sums.push(temp); } for (var k = 0; k < pointNum; ++k) { y0[k] = (max - sums[k]) / 2; } max = 0; for (var l = 0; l < pointNum; ++l) { var sum = sums[l] + y0[l]; if (sum > max) { max = sum; } } base.y0 = y0; base.max = max; return base; } /** * @file Visual encoding for themeRiver view * @author Deqing Li(annong035@gmail.com) */ var themeRiverVisual = function (ecModel) { ecModel.eachSeriesByType('themeRiver', function (seriesModel) { var data = seriesModel.getData(); var rawData = seriesModel.getRawData(); var colorList = seriesModel.get('color'); var idxMap = createHashMap(); data.each(function (idx) { idxMap.set(data.getRawIndex(idx), idx); }); rawData.each(function (rawIndex) { var name = rawData.getName(rawIndex); var color = colorList[(seriesModel.nameMap.get(name) - 1) % colorList.length]; rawData.setItemVisual(rawIndex, 'color', color); var idx = idxMap.get(rawIndex); if (idx != null) { data.setItemVisual(idx, 'color', color); } }); }); }; registerLayout(themeRiverLayout); registerVisual(themeRiverVisual); registerProcessor(dataFilter('themeRiver')); SeriesModel.extend({ type: 'series.sunburst', /** * @type {module:echarts/data/Tree~Node} */ _viewRoot: null, getInitialData: function (option, ecModel) { // Create a virtual root. var root = { name: option.name, children: option.data }; completeTreeValue$1(root); var levels = option.levels || []; // levels = option.levels = setDefault(levels, ecModel); var treeOption = {}; treeOption.levels = levels; // Make sure always a new tree is created when setOption, // in TreemapView, we check whether oldTree === newTree // to choose mappings approach among old shapes and new shapes. return Tree.createTree(root, this, treeOption).data; }, optionUpdated: function () { this.resetViewRoot(); }, /* * @override */ getDataParams: function (dataIndex) { var params = SeriesModel.prototype.getDataParams.apply(this, arguments); var node = this.getData().tree.getNodeByDataIndex(dataIndex); params.treePathInfo = wrapTreePathInfo(node, this); return params; }, defaultOption: { zlevel: 0, z: 2, // 默认全局居中 center: ['50%', '50%'], radius: [0, '75%'], // 默认顺时针 clockwise: true, startAngle: 90, // 最小角度改为0 minAngle: 0, percentPrecision: 2, // If still show when all data zero. stillShowZeroSum: true, // Policy of highlighting pieces when hover on one // Valid values: 'none' (for not downplay others), 'descendant', // 'ancestor', 'self' highlightPolicy: 'descendant', // 'rootToNode', 'link', or false nodeClick: 'rootToNode', renderLabelForZeroData: false, label: { // could be: 'radial', 'tangential', or 'none' rotate: 'radial', show: true, opacity: 1, // 'left' is for inner side of inside, and 'right' is for outter // side for inside align: 'center', position: 'inside', distance: 5, silent: true, emphasis: {} }, itemStyle: { borderWidth: 1, borderColor: 'white', opacity: 1, emphasis: {}, highlight: { opacity: 1 }, downplay: { opacity: 0.9 } }, // Animation type canbe expansion, scale animationType: 'expansion', animationDuration: 1000, animationDurationUpdate: 500, animationEasing: 'cubicOut', data: [], levels: [], /** * Sort order. * * Valid values: 'desc', 'asc', null, or callback function. * 'desc' and 'asc' for descend and ascendant order; * null for not sorting; * example of callback function: * function(nodeA, nodeB) { * return nodeA.getValue() - nodeB.getValue(); * } */ sort: 'desc' }, getViewRoot: function () { return this._viewRoot; }, /** * @param {module:echarts/data/Tree~Node} [viewRoot] */ resetViewRoot: function (viewRoot) { viewRoot ? (this._viewRoot = viewRoot) : (viewRoot = this._viewRoot); var root = this.getRawData().tree.root; if (!viewRoot || (viewRoot !== root && !root.contains(viewRoot)) ) { this._viewRoot = root; } } }); /** * @param {Object} dataNode */ function completeTreeValue$1(dataNode) { // Postorder travel tree. // If value of none-leaf node is not set, // calculate it by suming up the value of all children. var sum = 0; each$1(dataNode.children, function (child) { completeTreeValue$1(child); var childValue = child.value; isArray(childValue) && (childValue = childValue[0]); sum += childValue; }); var thisValue = dataNode.value; if (isArray(thisValue)) { thisValue = thisValue[0]; } if (thisValue == null || isNaN(thisValue)) { thisValue = sum; } // Value should not less than 0. if (thisValue < 0) { thisValue = 0; } isArray(dataNode.value) ? (dataNode.value[0] = thisValue) : (dataNode.value = thisValue); } var NodeHighlightPolicy = { NONE: 'none', // not downplay others DESCENDANT: 'descendant', ANCESTOR: 'ancestor', SELF: 'self' }; var DEFAULT_SECTOR_Z = 2; var DEFAULT_TEXT_Z = 4; /** * Sunburstce of Sunburst including Sector, Label, LabelLine * @constructor * @extends {module:zrender/graphic/Group} */ function SunburstPiece(node, seriesModel, ecModel) { Group.call(this); var sector = new Sector({ z2: DEFAULT_SECTOR_Z }); var text = new Text({ z2: DEFAULT_TEXT_Z, silent: node.getModel('label').get('silent') }); this.add(sector); this.add(text); this.updateData(true, node, 'normal', seriesModel, ecModel); // Hover to change label and labelLine function onEmphasis() { text.ignore = text.hoverIgnore; } function onNormal() { text.ignore = text.normalIgnore; } this.on('emphasis', onEmphasis) .on('normal', onNormal) .on('mouseover', onEmphasis) .on('mouseout', onNormal); } var SunburstPieceProto = SunburstPiece.prototype; SunburstPieceProto.updateData = function ( firstCreate, node, state, seriesModel, ecModel ) { this.node = node; node.piece = this; seriesModel = seriesModel || this._seriesModel; ecModel = ecModel || this._ecModel; var sector = this.childAt(0); sector.dataIndex = node.dataIndex; var itemModel = node.getModel(); var layout = node.getLayout(); var sectorShape = extend({}, layout); sectorShape.label = null; var visualColor = getNodeColor(node, seriesModel, ecModel); var normalStyle = itemModel.getModel('itemStyle').getItemStyle(); var style; if (state === 'normal') { style = normalStyle; } else { var stateStyle = itemModel.getModel(state + '.itemStyle') .getItemStyle(); style = merge(stateStyle, normalStyle); } style = defaults( { lineJoin: 'bevel', fill: style.fill || visualColor }, style ); if (firstCreate) { sector.setShape(sectorShape); sector.shape.r = layout.r0; updateProps( sector, { shape: { r: layout.r } }, seriesModel, node.dataIndex ); sector.useStyle(style); } else if (typeof style.fill === 'object' && style.fill.type || typeof sector.style.fill === 'object' && sector.style.fill.type ) { // Disable animation for gradient since no interpolation method // is supported for gradient updateProps(sector, { shape: sectorShape }, seriesModel); sector.useStyle(style); } else { updateProps(sector, { shape: sectorShape, style: style }, seriesModel); } this._updateLabel(seriesModel, visualColor, state); var cursorStyle = itemModel.getShallow('cursor'); cursorStyle && sector.attr('cursor', cursorStyle); if (firstCreate) { var highlightPolicy = seriesModel.getShallow('highlightPolicy'); this._initEvents(sector, node, seriesModel, highlightPolicy); } this._seriesModel = seriesModel || this._seriesModel; this._ecModel = ecModel || this._ecModel; }; SunburstPieceProto.onEmphasis = function (highlightPolicy) { var that = this; this.node.hostTree.root.eachNode(function (n) { if (n.piece) { if (that.node === n) { n.piece.updateData(false, n, 'emphasis'); } else if (isNodeHighlighted(n, that.node, highlightPolicy)) { n.piece.childAt(0).trigger('highlight'); } else if (highlightPolicy !== NodeHighlightPolicy.NONE) { n.piece.childAt(0).trigger('downplay'); } } }); }; SunburstPieceProto.onNormal = function () { this.node.hostTree.root.eachNode(function (n) { if (n.piece) { n.piece.updateData(false, n, 'normal'); } }); }; SunburstPieceProto.onHighlight = function () { this.updateData(false, this.node, 'highlight'); }; SunburstPieceProto.onDownplay = function () { this.updateData(false, this.node, 'downplay'); }; SunburstPieceProto._updateLabel = function (seriesModel, visualColor, state) { var itemModel = this.node.getModel(); var normalModel = itemModel.getModel('label'); var labelModel = state === 'normal' || state === 'emphasis' ? normalModel : itemModel.getModel(state + '.label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var text = retrieve( seriesModel.getFormattedLabel( this.node.dataIndex, 'normal', null, null, 'label' ), this.node.name ); if (getLabelAttr('show') === false) { text = ''; } var layout = this.node.getLayout(); var labelMinAngle = labelModel.get('minAngle'); if (labelMinAngle == null) { labelMinAngle = normalModel.get('minAngle'); } labelMinAngle = labelMinAngle / 180 * Math.PI; var angle = layout.endAngle - layout.startAngle; if (labelMinAngle != null && Math.abs(angle) < labelMinAngle) { // Not displaying text when angle is too small text = ''; } var label = this.childAt(1); setLabelStyle( label.style, label.hoverStyle || {}, normalModel, labelHoverModel, { defaultText: labelModel.getShallow('show') ? text : null, autoColor: visualColor, useInsideStyle: true } ); var midAngle = (layout.startAngle + layout.endAngle) / 2; var dx = Math.cos(midAngle); var dy = Math.sin(midAngle); var r; var labelPosition = getLabelAttr('position'); var labelPadding = getLabelAttr('distance') || 0; var textAlign = getLabelAttr('align'); if (labelPosition === 'outside') { r = layout.r + labelPadding; textAlign = midAngle > Math.PI / 2 ? 'right' : 'left'; } else { if (!textAlign || textAlign === 'center') { r = (layout.r + layout.r0) / 2; textAlign = 'center'; } else if (textAlign === 'left') { r = layout.r0 + labelPadding; if (midAngle > Math.PI / 2) { textAlign = 'right'; } } else if (textAlign === 'right') { r = layout.r - labelPadding; if (midAngle > Math.PI / 2) { textAlign = 'left'; } } } label.attr('style', { text: text, textAlign: textAlign, textVerticalAlign: getLabelAttr('verticalAlign') || 'middle', opacity: getLabelAttr('opacity') }); var textX = r * dx + layout.cx; var textY = r * dy + layout.cy; label.attr('position', [textX, textY]); var rotateType = getLabelAttr('rotate'); var rotate = 0; if (rotateType === 'radial') { rotate = -midAngle; if (rotate < -Math.PI / 2) { rotate += Math.PI; } } else if (rotateType === 'tangential') { rotate = Math.PI / 2 - midAngle; if (rotate > Math.PI / 2) { rotate -= Math.PI; } else if (rotate < -Math.PI / 2) { rotate += Math.PI; } } else if (typeof rotateType === 'number') { rotate = rotateType * Math.PI / 180; } label.attr('rotation', rotate); function getLabelAttr(name) { var stateAttr = labelModel.get(name); if (stateAttr == null) { return normalModel.get(name); } else { return stateAttr; } } }; SunburstPieceProto._initEvents = function ( sector, node, seriesModel, highlightPolicy ) { sector.off('mouseover').off('mouseout').off('emphasis').off('normal'); var that = this; var onEmphasis = function () { that.onEmphasis(highlightPolicy); }; var onNormal = function () { that.onNormal(); }; var onDownplay = function () { that.onDownplay(); }; var onHighlight = function () { that.onHighlight(); }; if (seriesModel.isAnimationEnabled()) { sector .on('mouseover', onEmphasis) .on('mouseout', onNormal) .on('emphasis', onEmphasis) .on('normal', onNormal) .on('downplay', onDownplay) .on('highlight', onHighlight); } }; inherits(SunburstPiece, Group); /** * Get node color * * @param {TreeNode} node the node to get color * @param {module:echarts/model/Series} seriesModel series * @param {module:echarts/model/Global} ecModel echarts defaults */ function getNodeColor(node, seriesModel, ecModel) { // Color from visualMap var visualColor = node.getVisual('color'); var visualMetaList = node.getVisual('visualMeta'); if (!visualMetaList || visualMetaList.length === 0) { // Use first-generation color if has no visualMap visualColor = null; } // Self color or level color var color = node.getModel('itemStyle').get('color'); if (color) { return color; } else if (visualColor) { // Color mapping return visualColor; } else if (node.depth === 0) { // Virtual root node return ecModel.option.color[0]; } else { // First-generation color var length = ecModel.option.color.length; color = ecModel.option.color[getRootId(node) % length]; } return color; } /** * Get index of root in sorted order * * @param {TreeNode} node current node * @return {number} index in root */ function getRootId(node) { var ancestor = node; while (ancestor.depth > 1) { ancestor = ancestor.parentNode; } var virtualRoot = node.getAncestors()[0]; return indexOf(virtualRoot.children, ancestor); } function isNodeHighlighted(node, activeNode, policy) { if (policy === NodeHighlightPolicy.NONE) { return false; } else if (policy === NodeHighlightPolicy.SELF) { return node === activeNode; } else if (policy === NodeHighlightPolicy.ANCESTOR) { return node === activeNode || node.isAncestorOf(activeNode); } else { return node === activeNode || node.isDescendantOf(activeNode); } } var ROOT_TO_NODE_ACTION = 'sunburstRootToNode'; var SunburstView = Chart.extend({ type: 'sunburst', init: function () { }, render: function (seriesModel, ecModel, api, payload) { var that = this; this.seriesModel = seriesModel; this.api = api; this.ecModel = ecModel; var data = seriesModel.getData(); var virtualRoot = data.tree.root; var newRoot = seriesModel.getViewRoot(); var group = this.group; var renderLabelForZeroData = seriesModel.get('renderLabelForZeroData'); var newChildren = []; newRoot.eachNode(function (node) { newChildren.push(node); }); var oldChildren = this._oldChildren || []; dualTravel(newChildren, oldChildren); renderRollUp(virtualRoot, newRoot); if (payload && payload.highlight && payload.highlight.piece) { var highlightPolicy = seriesModel.getShallow('highlightPolicy'); payload.highlight.piece.onEmphasis(highlightPolicy); } else if (payload && payload.unhighlight) { var piece = virtualRoot.piece; if (!piece && virtualRoot.children.length) { piece = virtualRoot.children[0].piece; } if (piece) { piece.onNormal(); } } this._initEvents(); this._oldChildren = newChildren; function dualTravel(newChildren, oldChildren) { if (newChildren.length === 0 && oldChildren.length === 0) { return; } new DataDiffer(oldChildren, newChildren, getKey, getKey) .add(processNode) .update(processNode) .remove(curry(processNode, null)) .execute(); function getKey(node) { return node.getId(); } function processNode(newId, oldId) { var newNode = newId == null ? null : newChildren[newId]; var oldNode = oldId == null ? null : oldChildren[oldId]; doRenderNode(newNode, oldNode); } } function doRenderNode(newNode, oldNode) { if (!renderLabelForZeroData && newNode && !newNode.getValue()) { // Not render data with value 0 newNode = null; } if (newNode !== virtualRoot && oldNode !== virtualRoot) { if (oldNode && oldNode.piece) { if (newNode) { // Update oldNode.piece.updateData( false, newNode, 'normal', seriesModel, ecModel); // For tooltip data.setItemGraphicEl(newNode.dataIndex, oldNode.piece); } else { // Remove removeNode(oldNode); } } else if (newNode) { // Add var piece = new SunburstPiece( newNode, seriesModel, ecModel ); group.add(piece); // For tooltip data.setItemGraphicEl(newNode.dataIndex, piece); } } } function removeNode(node) { if (!node) { return; } if (node.piece) { group.remove(node.piece); node.piece = null; } } function renderRollUp(virtualRoot, viewRoot) { if (viewRoot.depth > 0) { // Render if (virtualRoot.piece) { // Update virtualRoot.piece.updateData( false, virtualRoot, 'normal', seriesModel, ecModel); } else { // Add virtualRoot.piece = new SunburstPiece( virtualRoot, seriesModel, ecModel ); group.add(virtualRoot.piece); } if (viewRoot.piece._onclickEvent) { viewRoot.piece.off('click', viewRoot.piece._onclickEvent); } var event = function (e) { that._rootToNode(viewRoot.parentNode); }; viewRoot.piece._onclickEvent = event; virtualRoot.piece.on('click', event); } else if (virtualRoot.piece) { // Remove group.remove(virtualRoot.piece); virtualRoot.piece = null; } } }, dispose: function () { }, /** * @private */ _initEvents: function () { var that = this; var event = function (e) { var targetFound = false; var viewRoot = that.seriesModel.getViewRoot(); viewRoot.eachNode(function (node) { if (!targetFound && node.piece && node.piece.childAt(0) === e.target ) { var nodeClick = node.getModel().get('nodeClick'); if (nodeClick === 'rootToNode') { that._rootToNode(node); } else if (nodeClick === 'link') { var itemModel = node.getModel(); var link = itemModel.get('link'); if (link) { var linkTarget = itemModel.get('target', true) || '_blank'; window.open(link, linkTarget); } } targetFound = true; } }); }; if (this.group._onclickEvent) { this.group.off('click', this.group._onclickEvent); } this.group.on('click', event); this.group._onclickEvent = event; }, /** * @private */ _rootToNode: function (node) { if (node !== this.seriesModel.getViewRoot()) { this.api.dispatchAction({ type: ROOT_TO_NODE_ACTION, from: this.uid, seriesId: this.seriesModel.id, targetNode: node }); } }, /** * @implement */ containPoint: function (point, seriesModel) { var treeRoot = seriesModel.getData(); var itemLayout = treeRoot.getItemLayout(0); if (itemLayout) { var dx = point[0] - itemLayout.cx; var dy = point[1] - itemLayout.cy; var radius = Math.sqrt(dx * dx + dy * dy); return radius <= itemLayout.r && radius >= itemLayout.r0; } } }); /** * @file Sunburst action */ var ROOT_TO_NODE_ACTION$1 = 'sunburstRootToNode'; registerAction( {type: ROOT_TO_NODE_ACTION$1, update: 'updateView'}, function (payload, ecModel) { ecModel.eachComponent( {mainType: 'series', subType: 'sunburst', query: payload}, handleRootToNode ); function handleRootToNode(model, index) { var targetInfo = retrieveTargetInfo(payload, [ROOT_TO_NODE_ACTION$1], model); if (targetInfo) { var originViewRoot = model.getViewRoot(); if (originViewRoot) { payload.direction = aboveViewRoot(originViewRoot, targetInfo.node) ? 'rollUp' : 'drillDown'; } model.resetViewRoot(targetInfo.node); } } } ); var HIGHLIGHT_ACTION = 'sunburstHighlight'; registerAction( {type: HIGHLIGHT_ACTION, update: 'updateView'}, function (payload, ecModel) { ecModel.eachComponent( {mainType: 'series', subType: 'sunburst', query: payload}, handleHighlight ); function handleHighlight(model, index) { var targetInfo = retrieveTargetInfo(payload, [HIGHLIGHT_ACTION], model); if (targetInfo) { payload.highlight = targetInfo.node; } } } ); var UNHIGHLIGHT_ACTION = 'sunburstUnhighlight'; registerAction( {type: UNHIGHLIGHT_ACTION, update: 'updateView'}, function (payload, ecModel) { ecModel.eachComponent( {mainType: 'series', subType: 'sunburst', query: payload}, handleUnhighlight ); function handleUnhighlight(model, index) { payload.unhighlight = true; } } ); var RADIAN$1 = Math.PI / 180; var sunburstLayout = function (seriesType, ecModel, api, payload) { ecModel.eachSeriesByType(seriesType, function (seriesModel) { var center = seriesModel.get('center'); var radius = seriesModel.get('radius'); if (!isArray(radius)) { radius = [0, radius]; } if (!isArray(center)) { center = [center, center]; } var width = api.getWidth(); var height = api.getHeight(); var size = Math.min(width, height); var cx = parsePercent$1(center[0], width); var cy = parsePercent$1(center[1], height); var r0 = parsePercent$1(radius[0], size / 2); var r = parsePercent$1(radius[1], size / 2); var startAngle = -seriesModel.get('startAngle') * RADIAN$1; var minAngle = seriesModel.get('minAngle') * RADIAN$1; var virtualRoot = seriesModel.getData().tree.root; var treeRoot = seriesModel.getViewRoot(); var rootDepth = treeRoot.depth; var sort = seriesModel.get('sort'); if (sort != null) { initChildren$1(treeRoot, sort); } var validDataCount = 0; each$1(treeRoot.children, function (child) { !isNaN(child.getValue()) && validDataCount++; }); var sum = treeRoot.getValue(); // Sum may be 0 var unitRadian = Math.PI / (sum || validDataCount) * 2; var renderRollupNode = treeRoot.depth > 0; var levels = treeRoot.height - (renderRollupNode ? -1 : 1); var rPerLevel = (r - r0) / (levels || 1); var clockwise = seriesModel.get('clockwise'); var stillShowZeroSum = seriesModel.get('stillShowZeroSum'); // In the case some sector angle is smaller than minAngle var dir = clockwise ? 1 : -1; /** * Render a tree * @return increased angle */ var renderNode = function (node, startAngle) { if (!node) { return; } var endAngle = startAngle; // Render self if (node !== virtualRoot) { // Tree node is virtual, so it doesn't need to be drawn var value = node.getValue(); var angle = (sum === 0 && stillShowZeroSum) ? unitRadian : (value * unitRadian); if (angle < minAngle) { angle = minAngle; } else { } endAngle = startAngle + dir * angle; var depth = node.depth - rootDepth - (renderRollupNode ? -1 : 1); var rStart = r0 + rPerLevel * depth; var rEnd = r0 + rPerLevel * (depth + 1); var itemModel = node.getModel(); if (itemModel.get('r0') != null) { rStart = parsePercent$1(itemModel.get('r0'), size / 2); } if (itemModel.get('r') != null) { rEnd = parsePercent$1(itemModel.get('r'), size / 2); } node.setLayout({ angle: angle, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise, cx: cx, cy: cy, r0: rStart, r: rEnd }); } // Render children if (node.children && node.children.length) { // currentAngle = startAngle; var siblingAngle = 0; each$1(node.children, function (node) { siblingAngle += renderNode(node, startAngle + siblingAngle); }); } return endAngle - startAngle; }; // Virtual root node for roll up if (renderRollupNode) { var rStart = r0; var rEnd = r0 + rPerLevel; var angle = Math.PI * 2; virtualRoot.setLayout({ angle: angle, startAngle: startAngle, endAngle: startAngle + angle, clockwise: clockwise, cx: cx, cy: cy, r0: rStart, r: rEnd }); } renderNode(treeRoot, startAngle); }); }; /** * Init node children by order and update visual * * @param {TreeNode} node root node * @param {boolean} isAsc if is in ascendant order */ function initChildren$1(node, isAsc) { var children = node.children || []; node.children = sort$2(children, isAsc); // Init children recursively if (children.length) { each$1(node.children, function (child) { initChildren$1(child, isAsc); }); } } /** * Sort children nodes * * @param {TreeNode[]} children children of node to be sorted * @param {string | function | null} sort sort method * See SunburstSeries.js for details. */ function sort$2(children, sortOrder) { if (typeof sortOrder === 'function') { return children.sort(sortOrder); } else { var isAsc = sortOrder === 'asc'; return children.sort(function (a, b) { var diff = (a.getValue() - b.getValue()) * (isAsc ? 1 : -1); return diff === 0 ? (a.dataIndex - b.dataIndex) * (isAsc ? -1 : 1) : diff; }); } } registerVisual(curry(dataColor, 'sunburst')); registerLayout(curry(sunburstLayout, 'sunburst')); registerProcessor(curry(dataFilter, 'sunburst')); function dataToCoordSize(dataSize, dataItem) { // dataItem is necessary in log axis. dataItem = dataItem || [0, 0]; return map(['x', 'y'], function (dim, dimIdx) { var axis = this.getAxis(dim); var val = dataItem[dimIdx]; var halfSize = dataSize[dimIdx] / 2; return axis.type === 'category' ? axis.getBandWidth() : Math.abs(axis.dataToCoord(val - halfSize) - axis.dataToCoord(val + halfSize)); }, this); } var prepareCartesian2d = function (coordSys) { var rect = coordSys.grid.getRect(); return { coordSys: { // The name exposed to user is always 'cartesian2d' but not 'grid'. type: 'cartesian2d', x: rect.x, y: rect.y, width: rect.width, height: rect.height }, api: { coord: function (data) { // do not provide "out" param return coordSys.dataToPoint(data); }, size: bind(dataToCoordSize, coordSys) } }; }; function dataToCoordSize$1(dataSize, dataItem) { dataItem = dataItem || [0, 0]; return map([0, 1], function (dimIdx) { var val = dataItem[dimIdx]; var halfSize = dataSize[dimIdx] / 2; var p1 = []; var p2 = []; p1[dimIdx] = val - halfSize; p2[dimIdx] = val + halfSize; p1[1 - dimIdx] = p2[1 - dimIdx] = dataItem[1 - dimIdx]; return Math.abs(this.dataToPoint(p1)[dimIdx] - this.dataToPoint(p2)[dimIdx]); }, this); } var prepareGeo = function (coordSys) { var rect = coordSys.getBoundingRect(); return { coordSys: { type: 'geo', x: rect.x, y: rect.y, width: rect.width, height: rect.height }, api: { coord: function (data) { // do not provide "out" and noRoam param, // Compatible with this usage: // echarts.util.map(item.points, api.coord) return coordSys.dataToPoint(data); }, size: bind(dataToCoordSize$1, coordSys) } }; }; function dataToCoordSize$2(dataSize, dataItem) { // dataItem is necessary in log axis. var axis = this.getAxis(); var val = dataItem instanceof Array ? dataItem[0] : dataItem; var halfSize = (dataSize instanceof Array ? dataSize[0] : dataSize) / 2; return axis.type === 'category' ? axis.getBandWidth() : Math.abs(axis.dataToCoord(val - halfSize) - axis.dataToCoord(val + halfSize)); } var prepareSingleAxis = function (coordSys) { var rect = coordSys.getRect(); return { coordSys: { type: 'singleAxis', x: rect.x, y: rect.y, width: rect.width, height: rect.height }, api: { coord: function (val) { // do not provide "out" param return coordSys.dataToPoint(val); }, size: bind(dataToCoordSize$2, coordSys) } }; }; function dataToCoordSize$3(dataSize, dataItem) { // dataItem is necessary in log axis. return map(['Radius', 'Angle'], function (dim, dimIdx) { var axis = this['get' + dim + 'Axis'](); var val = dataItem[dimIdx]; var halfSize = dataSize[dimIdx] / 2; var method = 'dataTo' + dim; var result = axis.type === 'category' ? axis.getBandWidth() : Math.abs(axis[method](val - halfSize) - axis[method](val + halfSize)); if (dim === 'Angle') { result = result * Math.PI / 180; } return result; }, this); } var preparePolar = function (coordSys) { var radiusAxis = coordSys.getRadiusAxis(); var angleAxis = coordSys.getAngleAxis(); var radius = radiusAxis.getExtent(); radius[0] > radius[1] && radius.reverse(); return { coordSys: { type: 'polar', cx: coordSys.cx, cy: coordSys.cy, r: radius[1], r0: radius[0] }, api: { coord: bind(function (data) { var radius = radiusAxis.dataToRadius(data[0]); var angle = angleAxis.dataToAngle(data[1]); var coord = coordSys.coordToPoint([radius, angle]); coord.push(radius, angle * Math.PI / 180); return coord; }), size: bind(dataToCoordSize$3, coordSys) } }; }; var prepareCalendar = function (coordSys) { var rect = coordSys.getRect(); var rangeInfo = coordSys.getRangeInfo(); return { coordSys: { type: 'calendar', x: rect.x, y: rect.y, width: rect.width, height: rect.height, cellWidth: coordSys.getCellWidth(), cellHeight: coordSys.getCellHeight(), rangeInfo: { start: rangeInfo.start, end: rangeInfo.end, weeks: rangeInfo.weeks, dayCount: rangeInfo.allDay } }, api: { coord: function (data, clamp) { return coordSys.dataToPoint(data, clamp); } } }; }; var ITEM_STYLE_NORMAL_PATH = ['itemStyle']; var ITEM_STYLE_EMPHASIS_PATH = ['emphasis', 'itemStyle']; var LABEL_NORMAL = ['label']; var LABEL_EMPHASIS = ['emphasis', 'label']; // Use prefix to avoid index to be the same as el.name, // which will cause weird udpate animation. var GROUP_DIFF_PREFIX = 'e\0\0'; /** * To reduce total package size of each coordinate systems, the modules `prepareCustom` * of each coordinate systems are not required by each coordinate systems directly, but * required by the module `custom`. * * prepareInfoForCustomSeries {Function}: optional * @return {Object} {coordSys: {...}, api: { * coord: function (data, clamp) {}, // return point in global. * size: function (dataSize, dataItem) {} // return size of each axis in coordSys. * }} */ var prepareCustoms = { cartesian2d: prepareCartesian2d, geo: prepareGeo, singleAxis: prepareSingleAxis, polar: preparePolar, calendar: prepareCalendar }; // ------ // Model // ------ extendSeriesModel({ type: 'series.custom', dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'], defaultOption: { coordinateSystem: 'cartesian2d', // Can be set as 'none' zlevel: 0, z: 2, legendHoverLink: true // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, // Polar coordinate system // polarIndex: 0, // Geo coordinate system // geoIndex: 0, // label: {} // itemStyle: {} }, getInitialData: function (option, ecModel) { return createListFromArray(this.getSource(), this); } }); // ----- // View // ----- extendChartView({ type: 'custom', /** * @private * @type {module:echarts/data/List} */ _data: null, /** * @override */ render: function (customSeries, ecModel, api) { var oldData = this._data; var data = customSeries.getData(); var group = this.group; var renderItem = makeRenderItem(customSeries, data, ecModel, api); this.group.removeAll(); data.diff(oldData) .add(function (newIdx) { createOrUpdate$1( null, newIdx, renderItem(newIdx), customSeries, group, data ); }) .update(function (newIdx, oldIdx) { var el = oldData.getItemGraphicEl(oldIdx); createOrUpdate$1( el, newIdx, renderItem(newIdx), customSeries, group, data ); }) .remove(function (oldIdx) { var el = oldData.getItemGraphicEl(oldIdx); el && group.remove(el); }) .execute(); this._data = data; }, incrementalPrepareRender: function (customSeries, ecModel, api) { this.group.removeAll(); this._data = null; }, incrementalRender: function (params, customSeries, ecModel, api) { var data = customSeries.getData(); var renderItem = makeRenderItem(customSeries, data, ecModel, api); function setIncrementalAndHoverLayer(el) { if (!el.isGroup) { el.incremental = true; el.useHoverLayer = true; } } for (var idx = params.start; idx < params.end; idx++) { var el = createOrUpdate$1(null, idx, renderItem(idx), customSeries, this.group, data); el.traverse(setIncrementalAndHoverLayer); } }, /** * @override */ dispose: noop }); function createEl(elOption) { var graphicType = elOption.type; var el; if (graphicType === 'path') { var shape = elOption.shape; el = makePath( shape.pathData, null, { x: shape.x || 0, y: shape.y || 0, width: shape.width || 0, height: shape.height || 0 }, 'center' ); el.__customPathData = elOption.pathData; } else if (graphicType === 'image') { el = new ZImage({ }); el.__customImagePath = elOption.style.image; } else if (graphicType === 'text') { el = new Text({ }); el.__customText = elOption.style.text; } else { var Clz = graphic[graphicType.charAt(0).toUpperCase() + graphicType.slice(1)]; if (__DEV__) { assert$1(Clz, 'graphic type "' + graphicType + '" can not be found.'); } el = new Clz(); } el.__customGraphicType = graphicType; el.name = elOption.name; return el; } function updateEl(el, dataIndex, elOption, animatableModel, data, isInit) { var targetProps = {}; var elOptionStyle = elOption.style || {}; elOption.shape && (targetProps.shape = clone(elOption.shape)); elOption.position && (targetProps.position = elOption.position.slice()); elOption.scale && (targetProps.scale = elOption.scale.slice()); elOption.origin && (targetProps.origin = elOption.origin.slice()); elOption.rotation && (targetProps.rotation = elOption.rotation); if (el.type === 'image' && elOption.style) { var targetStyle = targetProps.style = {}; each$1(['x', 'y', 'width', 'height'], function (prop) { prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); }); } if (el.type === 'text' && elOption.style) { var targetStyle = targetProps.style = {}; each$1(['x', 'y'], function (prop) { prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); }); // Compatible with previous: both support // textFill and fill, textStroke and stroke in 'text' element. !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( elOptionStyle.textFill = elOptionStyle.fill ); !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( elOptionStyle.textStroke = elOptionStyle.stroke ); } if (el.type !== 'group') { el.useStyle(elOptionStyle); // Init animation. if (isInit) { el.style.opacity = 0; var targetOpacity = elOptionStyle.opacity; targetOpacity == null && (targetOpacity = 1); initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex); } } if (isInit) { el.attr(targetProps); } else { updateProps(el, targetProps, animatableModel, dataIndex); } // z2 must not be null/undefined, otherwise sort error may occur. el.attr({z2: elOption.z2 || 0, silent: elOption.silent}); elOption.styleEmphasis !== false && setHoverStyle(el, elOption.styleEmphasis); } function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) { if (elOptionStyle[prop] != null && !isInit) { targetStyle[prop] = elOptionStyle[prop]; elOptionStyle[prop] = oldElStyle[prop]; } } function makeRenderItem(customSeries, data, ecModel, api) { var renderItem = customSeries.get('renderItem'); var coordSys = customSeries.coordinateSystem; var prepareResult = {}; if (coordSys) { if (__DEV__) { assert$1(renderItem, 'series.render is required.'); assert$1( coordSys.prepareCustoms || prepareCustoms[coordSys.type], 'This coordSys does not support custom series.' ); } prepareResult = coordSys.prepareCustoms ? coordSys.prepareCustoms() : prepareCustoms[coordSys.type](coordSys); } var userAPI = defaults({ getWidth: api.getWidth, getHeight: api.getHeight, getZr: api.getZr, getDevicePixelRatio: api.getDevicePixelRatio, value: value, style: style, styleEmphasis: styleEmphasis, visual: visual, barLayout: barLayout, currentSeriesIndices: currentSeriesIndices, font: font }, prepareResult.api || {}); var userParams = { context: {}, seriesId: customSeries.id, seriesName: customSeries.name, seriesIndex: customSeries.seriesIndex, coordSys: prepareResult.coordSys, dataInsideLength: data.count(), encode: wrapEncodeDef(customSeries.getData()) }; // Do not support call `api` asynchronously without dataIndexInside input. var currDataIndexInside; var currDirty = true; var currItemModel; var currLabelNormalModel; var currLabelEmphasisModel; var currVisualColor; return function (dataIndexInside) { currDataIndexInside = dataIndexInside; currDirty = true; return renderItem && renderItem( defaults({ dataIndexInside: dataIndexInside, dataIndex: data.getRawIndex(dataIndexInside) }, userParams), userAPI ) || {}; }; // Do not update cache until api called. function updateCache(dataIndexInside) { dataIndexInside == null && (dataIndexInside = currDataIndexInside); if (currDirty) { currItemModel = data.getItemModel(dataIndexInside); currLabelNormalModel = currItemModel.getModel(LABEL_NORMAL); currLabelEmphasisModel = currItemModel.getModel(LABEL_EMPHASIS); currVisualColor = data.getItemVisual(dataIndexInside, 'color'); currDirty = false; } } /** * @public * @param {number|string} dim * @param {number} [dataIndexInside=currDataIndexInside] * @return {number|string} value */ function value(dim, dataIndexInside) { dataIndexInside == null && (dataIndexInside = currDataIndexInside); return data.get(data.getDimension(dim || 0), dataIndexInside); } /** * By default, `visual` is applied to style (to support visualMap). * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`, * it can be implemented as: * `api.style({stroke: api.visual('color'), fill: null})`; * @public * @param {Object} [extra] * @param {number} [dataIndexInside=currDataIndexInside] */ function style(extra, dataIndexInside) { dataIndexInside == null && (dataIndexInside = currDataIndexInside); updateCache(dataIndexInside); var itemStyle = currItemModel.getModel(ITEM_STYLE_NORMAL_PATH).getItemStyle(); currVisualColor != null && (itemStyle.fill = currVisualColor); var opacity = data.getItemVisual(dataIndexInside, 'opacity'); opacity != null && (itemStyle.opacity = opacity); setTextStyle(itemStyle, currLabelNormalModel, null, { autoColor: currVisualColor, isRectText: true }); itemStyle.text = currLabelNormalModel.getShallow('show') ? retrieve2( customSeries.getFormattedLabel(dataIndexInside, 'normal'), getDefaultLabel(data, dataIndexInside) ) : null; extra && extend(itemStyle, extra); return itemStyle; } /** * @public * @param {Object} [extra] * @param {number} [dataIndexInside=currDataIndexInside] */ function styleEmphasis(extra, dataIndexInside) { dataIndexInside == null && (dataIndexInside = currDataIndexInside); updateCache(dataIndexInside); var itemStyle = currItemModel.getModel(ITEM_STYLE_EMPHASIS_PATH).getItemStyle(); setTextStyle(itemStyle, currLabelEmphasisModel, null, { isRectText: true }, true); itemStyle.text = currLabelEmphasisModel.getShallow('show') ? retrieve3( customSeries.getFormattedLabel(dataIndexInside, 'emphasis'), customSeries.getFormattedLabel(dataIndexInside, 'normal'), getDefaultLabel(data, dataIndexInside) ) : null; extra && extend(itemStyle, extra); return itemStyle; } /** * @public * @param {string} visualType * @param {number} [dataIndexInside=currDataIndexInside] */ function visual(visualType, dataIndexInside) { dataIndexInside == null && (dataIndexInside = currDataIndexInside); return data.getItemVisual(dataIndexInside, visualType); } /** * @public * @param {number} opt.count Positive interger. * @param {number} [opt.barWidth] * @param {number} [opt.barMaxWidth] * @param {number} [opt.barGap] * @param {number} [opt.barCategoryGap] * @return {Object} {width, offset, offsetCenter} is not support, return undefined. */ function barLayout(opt) { if (coordSys.getBaseAxis) { var baseAxis = coordSys.getBaseAxis(); return getLayoutOnAxis(defaults({axis: baseAxis}, opt), api); } } /** * @public * @return {Array.<number>} */ function currentSeriesIndices() { return ecModel.getCurrentSeriesIndices(); } /** * @public * @param {Object} opt * @param {string} [opt.fontStyle] * @param {number} [opt.fontWeight] * @param {number} [opt.fontSize] * @param {string} [opt.fontFamily] * @return {string} font string */ function font(opt) { return getFont(opt, ecModel); } } function wrapEncodeDef(data) { var encodeDef = {}; each$1(data.dimensions, function (dimName, dataDimIndex) { var dimInfo = data.getDimensionInfo(dimName); if (!dimInfo.isExtraCoord) { var coordDim = dimInfo.coordDim; var dataDims = encodeDef[coordDim] = encodeDef[coordDim] || []; dataDims[dimInfo.coordDimIndex] = dataDimIndex; } }); return encodeDef; } function createOrUpdate$1(el, dataIndex, elOption, animatableModel, group, data) { el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data); el && data.setItemGraphicEl(dataIndex, el); return el; } function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data) { var elOptionType = elOption.type; if (el && elOptionType !== el.__customGraphicType && (elOptionType !== 'path' || elOption.pathData !== el.__customPathData) && (elOptionType !== 'image' || elOption.style.image !== el.__customImagePath) && (elOptionType !== 'text' || elOption.style.text !== el.__customText) ) { group.remove(el); el = null; } // `elOption.type` is undefined when `renderItem` returns nothing. if (elOptionType == null) { return; } var isInit = !el; !el && (el = createEl(elOption)); updateEl(el, dataIndex, elOption, animatableModel, data, isInit); if (elOptionType === 'group') { var oldChildren = el.children() || []; var newChildren = elOption.children || []; if (elOption.diffChildrenByName) { // lower performance. diffGroupChildren({ oldChildren: oldChildren, newChildren: newChildren, dataIndex: dataIndex, animatableModel: animatableModel, group: el, data: data }); } else { // better performance. var index = 0; for (; index < newChildren.length; index++) { doCreateOrUpdate( el.childAt(index), dataIndex, newChildren[index], animatableModel, el, data ); } for (; index < oldChildren.length; index++) { oldChildren[index] && el.remove(oldChildren[index]); } } } group.add(el); return el; } function diffGroupChildren(context) { (new DataDiffer( context.oldChildren, context.newChildren, getKey, getKey, context )) .add(processAddUpdate) .update(processAddUpdate) .remove(processRemove) .execute(); } function getKey(item, idx) { var name = item && item.name; return name != null ? name : GROUP_DIFF_PREFIX + idx; } function processAddUpdate(newIndex, oldIndex) { var context = this.context; var childOption = newIndex != null ? context.newChildren[newIndex] : null; var child = oldIndex != null ? context.oldChildren[oldIndex] : null; doCreateOrUpdate( child, context.dataIndex, childOption, context.animatableModel, context.group, context.data ); } function processRemove(oldIndex) { var context = this.context; var child = context.oldChildren[oldIndex]; child && context.group.remove(child); } // ------------- // Preprocessor // ------------- registerPreprocessor(function (option) { var graphicOption = option.graphic; // Convert // {graphic: [{left: 10, type: 'circle'}, ...]} // or // {graphic: {left: 10, type: 'circle'}} // to // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]} if (isArray(graphicOption)) { if (!graphicOption[0] || !graphicOption[0].elements) { option.graphic = [{elements: graphicOption}]; } else { // Only one graphic instance can be instantiated. (We dont // want that too many views are created in echarts._viewMap) option.graphic = [option.graphic[0]]; } } else if (graphicOption && !graphicOption.elements) { option.graphic = [{elements: [graphicOption]}]; } }); // ------ // Model // ------ var GraphicModel = extendComponentModel({ type: 'graphic', defaultOption: { // Extra properties for each elements: // // left/right/top/bottom: (like 12, '22%', 'center', default undefined) // If left/rigth is set, shape.x/shape.cx/position will not be used. // If top/bottom is set, shape.y/shape.cy/position will not be used. // This mechanism is useful when you want to position a group/element // against the right side or the center of this container. // // width/height: (can only be pixel value, default 0) // Only be used to specify contianer(group) size, if needed. And // can not be percentage value (like '33%'). See the reason in the // layout algorithm below. // // bounding: (enum: 'all' (default) | 'raw') // Specify how to calculate boundingRect when locating. // 'all': Get uioned and transformed boundingRect // from both itself and its descendants. // This mode simplies confining a group of elements in the bounding // of their ancester container (e.g., using 'right: 0'). // 'raw': Only use the boundingRect of itself and before transformed. // This mode is similar to css behavior, which is useful when you // want an element to be able to overflow its container. (Consider // a rotated circle needs to be located in a corner.) // Note: elements is always behind its ancestors in this elements array. elements: [], parentId: null }, /** * Save el options for the sake of the performance (only update modified graphics). * The order is the same as those in option. (ancesters -> descendants) * * @private * @type {Array.<Object>} */ _elOptionsToUpdate: null, /** * @override */ mergeOption: function (option) { // Prevent default merge to elements var elements = this.option.elements; this.option.elements = null; GraphicModel.superApply(this, 'mergeOption', arguments); this.option.elements = elements; }, /** * @override */ optionUpdated: function (newOption, isInit) { var thisOption = this.option; var newList = (isInit ? thisOption : newOption).elements; var existList = thisOption.elements = isInit ? [] : thisOption.elements; var flattenedList = []; this._flatten(newList, flattenedList); var mappingResult = mappingToExists(existList, flattenedList); makeIdAndName(mappingResult); // Clear elOptionsToUpdate var elOptionsToUpdate = this._elOptionsToUpdate = []; each$1(mappingResult, function (resultItem, index) { var newElOption = resultItem.option; if (__DEV__) { assert$1( isObject$1(newElOption) || resultItem.exist, 'Empty graphic option definition' ); } if (!newElOption) { return; } elOptionsToUpdate.push(newElOption); setKeyInfoToNewElOption(resultItem, newElOption); mergeNewElOptionToExist(existList, index, newElOption); setLayoutInfoToExist(existList[index], newElOption); }, this); // Clean for (var i = existList.length - 1; i >= 0; i--) { if (existList[i] == null) { existList.splice(i, 1); } else { // $action should be volatile, otherwise option gotten from // `getOption` will contain unexpected $action. delete existList[i].$action; } } }, /** * Convert * [{ * type: 'group', * id: 'xx', * children: [{type: 'circle'}, {type: 'polygon'}] * }] * to * [ * {type: 'group', id: 'xx'}, * {type: 'circle', parentId: 'xx'}, * {type: 'polygon', parentId: 'xx'} * ] * * @private * @param {Array.<Object>} optionList option list * @param {Array.<Object>} result result of flatten * @param {Object} parentOption parent option */ _flatten: function (optionList, result, parentOption) { each$1(optionList, function (option) { if (!option) { return; } if (parentOption) { option.parentOption = parentOption; } result.push(option); var children = option.children; if (option.type === 'group' && children) { this._flatten(children, result, option); } // Deleting for JSON output, and for not affecting group creation. delete option.children; }, this); }, // FIXME // Pass to view using payload? setOption has a payload? useElOptionsToUpdate: function () { var els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming. this._elOptionsToUpdate = null; return els; } }); // ----- // View // ----- extendComponentView({ type: 'graphic', /** * @override */ init: function (ecModel, api) { /** * @private * @type {module:zrender/core/util.HashMap} */ this._elMap = createHashMap(); /** * @private * @type {module:echarts/graphic/GraphicModel} */ this._lastGraphicModel; }, /** * @override */ render: function (graphicModel, ecModel, api) { // Having leveraged between use cases and algorithm complexity, a very // simple layout mechanism is used: // The size(width/height) can be determined by itself or its parent (not // implemented yet), but can not by its children. (Top-down travel) // The location(x/y) can be determined by the bounding rect of itself // (can including its descendants or not) and the size of its parent. // (Bottom-up travel) // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, // view will be reused. if (graphicModel !== this._lastGraphicModel) { this._clear(); } this._lastGraphicModel = graphicModel; this._updateElements(graphicModel, api); this._relocate(graphicModel, api); }, /** * Update graphic elements. * * @private * @param {Object} graphicModel graphic model * @param {module:echarts/ExtensionAPI} api extension API */ _updateElements: function (graphicModel, api) { var elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); if (!elOptionsToUpdate) { return; } var elMap = this._elMap; var rootGroup = this.group; // Top-down tranverse to assign graphic settings to each elements. each$1(elOptionsToUpdate, function (elOption) { var $action = elOption.$action; var id = elOption.id; var existEl = elMap.get(id); var parentId = elOption.parentId; var targetElParent = parentId != null ? elMap.get(parentId) : rootGroup; if (elOption.type === 'text') { var elOptionStyle = elOption.style; // In top/bottom mode, textVerticalAlign should not be used, which cause // inaccurately locating. if (elOption.hv && elOption.hv[1]) { elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null; } // Compatible with previous setting: both support fill and textFill, // stroke and textStroke. !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( elOptionStyle.textFill = elOptionStyle.fill ); !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( elOptionStyle.textStroke = elOptionStyle.stroke ); } // Remove unnecessary props to avoid potential problems. var elOptionCleaned = getCleanedElOption(elOption); // For simple, do not support parent change, otherwise reorder is needed. if (__DEV__) { existEl && assert$1( targetElParent === existEl.parent, 'Changing parent is not supported.' ); } if (!$action || $action === 'merge') { existEl ? existEl.attr(elOptionCleaned) : createEl$1(id, targetElParent, elOptionCleaned, elMap); } else if ($action === 'replace') { removeEl(existEl, elMap); createEl$1(id, targetElParent, elOptionCleaned, elMap); } else if ($action === 'remove') { removeEl(existEl, elMap); } var el = elMap.get(id); if (el) { el.__ecGraphicWidth = elOption.width; el.__ecGraphicHeight = elOption.height; } }); }, /** * Locate graphic elements. * * @private * @param {Object} graphicModel graphic model * @param {module:echarts/ExtensionAPI} api extension API */ _relocate: function (graphicModel, api) { var elOptions = graphicModel.option.elements; var rootGroup = this.group; var elMap = this._elMap; // Bottom-up tranvese all elements (consider ec resize) to locate elements. for (var i = elOptions.length - 1; i >= 0; i--) { var elOption = elOptions[i]; var el = elMap.get(elOption.id); if (!el) { continue; } var parentEl = el.parent; var containerInfo = parentEl === rootGroup ? { width: api.getWidth(), height: api.getHeight() } : { // Like 'position:absolut' in css, default 0. width: parentEl.__ecGraphicWidth || 0, height: parentEl.__ecGraphicHeight || 0 }; positionElement( el, elOption, containerInfo, null, {hv: elOption.hv, boundingMode: elOption.bounding} ); } }, /** * Clear all elements. * * @private */ _clear: function () { var elMap = this._elMap; elMap.each(function (el) { removeEl(el, elMap); }); this._elMap = createHashMap(); }, /** * @override */ dispose: function () { this._clear(); } }); function createEl$1(id, targetElParent, elOption, elMap) { var graphicType = elOption.type; if (__DEV__) { assert$1(graphicType, 'graphic type MUST be set'); } var Clz = graphic[graphicType.charAt(0).toUpperCase() + graphicType.slice(1)]; if (__DEV__) { assert$1(Clz, 'graphic type can not be found'); } var el = new Clz(elOption); targetElParent.add(el); elMap.set(id, el); el.__ecGraphicId = id; } function removeEl(existEl, elMap) { var existElParent = existEl && existEl.parent; if (existElParent) { existEl.type === 'group' && existEl.traverse(function (el) { removeEl(el, elMap); }); elMap.removeKey(existEl.__ecGraphicId); existElParent.remove(existEl); } } // Remove unnecessary props to avoid potential problems. function getCleanedElOption(elOption) { elOption = extend({}, elOption); each$1( ['id', 'parentId', '$action', 'hv', 'bounding'].concat(LOCATION_PARAMS), function (name) { delete elOption[name]; } ); return elOption; } function isSetLoc(obj, props) { var isSet; each$1(props, function (prop) { obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); }); return isSet; } function setKeyInfoToNewElOption(resultItem, newElOption) { var existElOption = resultItem.exist; // Set id and type after id assigned. newElOption.id = resultItem.keyInfo.id; !newElOption.type && existElOption && (newElOption.type = existElOption.type); // Set parent id if not specified if (newElOption.parentId == null) { var newElParentOption = newElOption.parentOption; if (newElParentOption) { newElOption.parentId = newElParentOption.id; } else if (existElOption) { newElOption.parentId = existElOption.parentId; } } // Clear newElOption.parentOption = null; } function mergeNewElOptionToExist(existList, index, newElOption) { // Update existing options, for `getOption` feature. var newElOptCopy = extend({}, newElOption); var existElOption = existList[index]; var $action = newElOption.$action || 'merge'; if ($action === 'merge') { if (existElOption) { if (__DEV__) { var newType = newElOption.type; assert$1( !newType || existElOption.type === newType, 'Please set $action: "replace" to change `type`' ); } // We can ensure that newElOptCopy and existElOption are not // the same object, so `merge` will not change newElOptCopy. merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize. mergeLayoutParam(existElOption, newElOptCopy, {ignoreSize: true}); // Will be used in render. copyLayoutParams(newElOption, existElOption); } else { existList[index] = newElOptCopy; } } else if ($action === 'replace') { existList[index] = newElOptCopy; } else if ($action === 'remove') { // null will be cleaned later. existElOption && (existList[index] = null); } } function setLayoutInfoToExist(existItem, newElOption) { if (!existItem) { return; } existItem.hv = newElOption.hv = [ // Rigid body, dont care `width`. isSetLoc(newElOption, ['left', 'right']), // Rigid body, dont care `height`. isSetLoc(newElOption, ['top', 'bottom']) ]; // Give default group size. Otherwise layout error may occur. if (existItem.type === 'group') { existItem.width == null && (existItem.width = newElOption.width = 0); existItem.height == null && (existItem.height = newElOption.height = 0); } } var LegendModel = extendComponentModel({ type: 'legend.plain', dependencies: ['series'], layoutMode: { type: 'box', // legend.width/height are maxWidth/maxHeight actually, // whereas realy width/height is calculated by its content. // (Setting {left: 10, right: 10} does not make sense). // So consider the case: // `setOption({legend: {left: 10});` // then `setOption({legend: {right: 10});` // The previous `left` should be cleared by setting `ignoreSize`. ignoreSize: true }, init: function (option, parentModel, ecModel) { this.mergeDefaultAndTheme(option, ecModel); option.selected = option.selected || {}; }, mergeOption: function (option) { LegendModel.superCall(this, 'mergeOption', option); }, optionUpdated: function () { this._updateData(this.ecModel); var legendData = this._data; // If selectedMode is single, try to select one if (legendData[0] && this.get('selectedMode') === 'single') { var hasSelected = false; // If has any selected in option.selected for (var i = 0; i < legendData.length; i++) { var name = legendData[i].get('name'); if (this.isSelected(name)) { // Force to unselect others this.select(name); hasSelected = true; break; } } // Try select the first if selectedMode is single !hasSelected && this.select(legendData[0].get('name')); } }, _updateData: function (ecModel) { var potentialData = []; var availableNames = []; ecModel.eachRawSeries(function (seriesModel) { var seriesName = seriesModel.name; availableNames.push(seriesName); var isPotential; if (seriesModel.legendDataProvider) { var data = seriesModel.legendDataProvider(); var names = data.mapArray(data.getName); if (!ecModel.isSeriesFiltered(seriesModel)) { availableNames = availableNames.concat(names); } if (names.length) { potentialData = potentialData.concat(names); } else { isPotential = true; } } else { isPotential = true; } if (isPotential && isNameSpecified(seriesModel)) { potentialData.push(seriesModel.name); } }); /** * @type {Array.<string>} * @private */ this._availableNames = availableNames; // If legend.data not specified in option, use availableNames as data, // which is convinient for user preparing option. var rawData = this.get('data') || potentialData; var legendData = map(rawData, function (dataItem) { // Can be string or number if (typeof dataItem === 'string' || typeof dataItem === 'number') { dataItem = { name: dataItem }; } return new Model(dataItem, this, this.ecModel); }, this); /** * @type {Array.<module:echarts/model/Model>} * @private */ this._data = legendData; }, /** * @return {Array.<module:echarts/model/Model>} */ getData: function () { return this._data; }, /** * @param {string} name */ select: function (name) { var selected = this.option.selected; var selectedMode = this.get('selectedMode'); if (selectedMode === 'single') { var data = this._data; each$1(data, function (dataItem) { selected[dataItem.get('name')] = false; }); } selected[name] = true; }, /** * @param {string} name */ unSelect: function (name) { if (this.get('selectedMode') !== 'single') { this.option.selected[name] = false; } }, /** * @param {string} name */ toggleSelected: function (name) { var selected = this.option.selected; // Default is true if (!selected.hasOwnProperty(name)) { selected[name] = true; } this[selected[name] ? 'unSelect' : 'select'](name); }, /** * @param {string} name */ isSelected: function (name) { var selected = this.option.selected; return !(selected.hasOwnProperty(name) && !selected[name]) && indexOf(this._availableNames, name) >= 0; }, defaultOption: { // 一级层叠 zlevel: 0, // 二级层叠 z: 4, show: true, // 布局方式,默认为水平布局,可选为: // 'horizontal' | 'vertical' orient: 'horizontal', left: 'center', // right: 'center', top: 0, // bottom: null, // 水平对齐 // 'auto' | 'left' | 'right' // 默认为 'auto', 根据 x 的位置判断是左对齐还是右对齐 align: 'auto', backgroundColor: 'rgba(0,0,0,0)', // 图例边框颜色 borderColor: '#ccc', borderRadius: 0, // 图例边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 图例内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // 各个item之间的间隔,单位px,默认为10, // 横向布局时为水平间隔,纵向布局时为纵向间隔 itemGap: 10, // 图例图形宽度 itemWidth: 25, // 图例图形高度 itemHeight: 14, // 图例关闭时候的颜色 inactiveColor: '#ccc', textStyle: { // 图例文字颜色 color: '#333' }, // formatter: '', // 选择模式,默认开启图例开关 selectedMode: true, // 配置默认选中状态,可配合LEGEND.SELECTED事件做动态数据载入 // selected: null, // 图例内容(详见legend.data,数组中每一项代表一个item // data: [], // Tooltip 相关配置 tooltip: { show: false } } }); function legendSelectActionHandler(methodName, payload, ecModel) { var selectedMap = {}; var isToggleSelect = methodName === 'toggleSelected'; var isSelected; // Update all legend components ecModel.eachComponent('legend', function (legendModel) { if (isToggleSelect && isSelected != null) { // Force other legend has same selected status // Or the first is toggled to true and other are toggled to false // In the case one legend has some item unSelected in option. And if other legend // doesn't has the item, they will assume it is selected. legendModel[isSelected ? 'select' : 'unSelect'](payload.name); } else { legendModel[methodName](payload.name); isSelected = legendModel.isSelected(payload.name); } var legendData = legendModel.getData(); each$1(legendData, function (model) { var name = model.get('name'); // Wrap element if (name === '\n' || name === '') { return; } var isItemSelected = legendModel.isSelected(name); if (selectedMap.hasOwnProperty(name)) { // Unselected if any legend is unselected selectedMap[name] = selectedMap[name] && isItemSelected; } else { selectedMap[name] = isItemSelected; } }); }); // Return the event explicitly return { name: payload.name, selected: selectedMap }; } /** * @event legendToggleSelect * @type {Object} * @property {string} type 'legendToggleSelect' * @property {string} [from] * @property {string} name Series name or data item name */ registerAction( 'legendToggleSelect', 'legendselectchanged', curry(legendSelectActionHandler, 'toggleSelected') ); /** * @event legendSelect * @type {Object} * @property {string} type 'legendSelect' * @property {string} name Series name or data item name */ registerAction( 'legendSelect', 'legendselected', curry(legendSelectActionHandler, 'select') ); /** * @event legendUnSelect * @type {Object} * @property {string} type 'legendUnSelect' * @property {string} name Series name or data item name */ registerAction( 'legendUnSelect', 'legendunselected', curry(legendSelectActionHandler, 'unSelect') ); /** * Layout list like component. * It will box layout each items in group of component and then position the whole group in the viewport * @param {module:zrender/group/Group} group * @param {module:echarts/model/Component} componentModel * @param {module:echarts/ExtensionAPI} */ function layout$3(group, componentModel, api) { var boxLayoutParams = componentModel.getBoxLayoutParams(); var padding = componentModel.get('padding'); var viewportSize = {width: api.getWidth(), height: api.getHeight()}; var rect = getLayoutRect( boxLayoutParams, viewportSize, padding ); box( componentModel.get('orient'), group, componentModel.get('itemGap'), rect.width, rect.height ); positionElement( group, boxLayoutParams, viewportSize, padding ); } function makeBackground(rect, componentModel) { var padding = normalizeCssArray$1( componentModel.get('padding') ); var style = componentModel.getItemStyle(['color', 'opacity']); style.fill = componentModel.get('backgroundColor'); var rect = new Rect({ shape: { x: rect.x - padding[3], y: rect.y - padding[0], width: rect.width + padding[1] + padding[3], height: rect.height + padding[0] + padding[2], r: componentModel.get('borderRadius') }, style: style, silent: true, z2: -1 }); // FIXME // `subPixelOptimizeRect` may bring some gap between edge of viewpart // and background rect when setting like `left: 0`, `top: 0`. // graphic.subPixelOptimizeRect(rect); return rect; } var curry$4 = curry; var each$17 = each$1; var Group$3 = Group; var LegendView = extendComponentView({ type: 'legend.plain', newlineDisabled: false, /** * @override */ init: function () { /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._contentGroup = new Group$3()); /** * @private * @type {module:zrender/Element} */ this._backgroundEl; }, /** * @protected */ getContentGroup: function () { return this._contentGroup; }, /** * @override */ render: function (legendModel, ecModel, api) { this.resetInner(); if (!legendModel.get('show', true)) { return; } var itemAlign = legendModel.get('align'); if (!itemAlign || itemAlign === 'auto') { itemAlign = ( legendModel.get('left') === 'right' && legendModel.get('orient') === 'vertical' ) ? 'right' : 'left'; } this.renderInner(itemAlign, legendModel, ecModel, api); // Perform layout. var positionInfo = legendModel.getBoxLayoutParams(); var viewportSize = {width: api.getWidth(), height: api.getHeight()}; var padding = legendModel.get('padding'); var maxSize = getLayoutRect(positionInfo, viewportSize, padding); var mainRect = this.layoutInner(legendModel, itemAlign, maxSize); // Place mainGroup, based on the calculated `mainRect`. var layoutRect = getLayoutRect( defaults({width: mainRect.width, height: mainRect.height}, positionInfo), viewportSize, padding ); this.group.attr('position', [layoutRect.x - mainRect.x, layoutRect.y - mainRect.y]); // Render background after group is layout. this.group.add( this._backgroundEl = makeBackground(mainRect, legendModel) ); }, /** * @protected */ resetInner: function () { this.getContentGroup().removeAll(); this._backgroundEl && this.group.remove(this._backgroundEl); }, /** * @protected */ renderInner: function (itemAlign, legendModel, ecModel, api) { var contentGroup = this.getContentGroup(); var legendDrawnMap = createHashMap(); var selectMode = legendModel.get('selectedMode'); each$17(legendModel.getData(), function (itemModel, dataIndex) { var name = itemModel.get('name'); // Use empty string or \n as a newline string if (!this.newlineDisabled && (name === '' || name === '\n')) { contentGroup.add(new Group$3({ newline: true })); return; } var seriesModel = ecModel.getSeriesByName(name)[0]; if (legendDrawnMap.get(name)) { // Have been drawed return; } // Series legend if (seriesModel) { var data = seriesModel.getData(); var color = data.getVisual('color'); // If color is a callback function if (typeof color === 'function') { // Use the first data color = color(seriesModel.getDataParams(0)); } // Using rect symbol defaultly var legendSymbolType = data.getVisual('legendSymbol') || 'roundRect'; var symbolType = data.getVisual('symbol'); var itemGroup = this._createItem( name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, selectMode ); itemGroup.on('click', curry$4(dispatchSelectAction, name, api)) .on('mouseover', curry$4(dispatchHighlightAction, seriesModel, null, api)) .on('mouseout', curry$4(dispatchDownplayAction, seriesModel, null, api)); legendDrawnMap.set(name, true); } else { // Data legend of pie, funnel ecModel.eachRawSeries(function (seriesModel) { // In case multiple series has same data name if (legendDrawnMap.get(name)) { return; } if (seriesModel.legendDataProvider) { var data = seriesModel.legendDataProvider(); var idx = data.indexOfName(name); if (idx < 0) { return; } var color = data.getItemVisual(idx, 'color'); var legendSymbolType = 'roundRect'; var itemGroup = this._createItem( name, dataIndex, itemModel, legendModel, legendSymbolType, null, itemAlign, color, selectMode ); itemGroup.on('click', curry$4(dispatchSelectAction, name, api)) // FIXME Should not specify the series name .on('mouseover', curry$4(dispatchHighlightAction, seriesModel, name, api)) .on('mouseout', curry$4(dispatchDownplayAction, seriesModel, name, api)); legendDrawnMap.set(name, true); } }, this); } if (__DEV__) { if (!legendDrawnMap.get(name)) { console.warn(name + ' series not exists. Legend data should be same with series name or data name.'); } } }, this); }, _createItem: function ( name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, selectMode ) { var itemWidth = legendModel.get('itemWidth'); var itemHeight = legendModel.get('itemHeight'); var inactiveColor = legendModel.get('inactiveColor'); var isSelected = legendModel.isSelected(name); var itemGroup = new Group$3(); var textStyleModel = itemModel.getModel('textStyle'); var itemIcon = itemModel.get('icon'); var tooltipModel = itemModel.getModel('tooltip'); var legendGlobalTooltipModel = tooltipModel.parentModel; // Use user given icon first legendSymbolType = itemIcon || legendSymbolType; itemGroup.add(createSymbol( legendSymbolType, 0, 0, itemWidth, itemHeight, isSelected ? color : inactiveColor, true )); // Compose symbols // PENDING if (!itemIcon && symbolType // At least show one symbol, can't be all none && ((symbolType !== legendSymbolType) || symbolType == 'none') ) { var size = itemHeight * 0.8; if (symbolType === 'none') { symbolType = 'circle'; } // Put symbol in the center itemGroup.add(createSymbol( symbolType, (itemWidth - size) / 2, (itemHeight - size) / 2, size, size, isSelected ? color : inactiveColor )); } var textX = itemAlign === 'left' ? itemWidth + 5 : -5; var textAlign = itemAlign; var formatter = legendModel.get('formatter'); var content = name; if (typeof formatter === 'string' && formatter) { content = formatter.replace('{name}', name != null ? name : ''); } else if (typeof formatter === 'function') { content = formatter(name); } itemGroup.add(new Text({ style: setTextStyle({}, textStyleModel, { text: content, x: textX, y: itemHeight / 2, textFill: isSelected ? textStyleModel.getTextColor() : inactiveColor, textAlign: textAlign, textVerticalAlign: 'middle' }) })); // Add a invisible rect to increase the area of mouse hover var hitRect = new Rect({ shape: itemGroup.getBoundingRect(), invisible: true, tooltip: tooltipModel.get('show') ? extend({ content: name, // Defaul formatter formatter: legendGlobalTooltipModel.get('formatter', true) || function () { return name; }, formatterParams: { componentType: 'legend', legendIndex: legendModel.componentIndex, name: name, $vars: ['name'] } }, tooltipModel.option) : null }); itemGroup.add(hitRect); itemGroup.eachChild(function (child) { child.silent = true; }); hitRect.silent = !selectMode; this.getContentGroup().add(itemGroup); setHoverStyle(itemGroup); itemGroup.__legendDataIndex = dataIndex; return itemGroup; }, /** * @protected */ layoutInner: function (legendModel, itemAlign, maxSize) { var contentGroup = this.getContentGroup(); // Place items in contentGroup. box( legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), maxSize.width, maxSize.height ); var contentRect = contentGroup.getBoundingRect(); contentGroup.attr('position', [-contentRect.x, -contentRect.y]); return this.group.getBoundingRect(); } }); function dispatchSelectAction(name, api) { api.dispatchAction({ type: 'legendToggleSelect', name: name }); } function dispatchHighlightAction(seriesModel, dataName, api) { // If element hover will move to a hoverLayer. var el = api.getZr().storage.getDisplayList()[0]; if (!(el && el.useHoverLayer)) { seriesModel.get('legendHoverLink') && api.dispatchAction({ type: 'highlight', seriesName: seriesModel.name, name: dataName }); } } function dispatchDownplayAction(seriesModel, dataName, api) { // If element hover will move to a hoverLayer. var el = api.getZr().storage.getDisplayList()[0]; if (!(el && el.useHoverLayer)) { seriesModel.get('legendHoverLink') && api.dispatchAction({ type: 'downplay', seriesName: seriesModel.name, name: dataName }); } } var legendFilter = function (ecModel) { var legendModels = ecModel.findComponents({ mainType: 'legend' }); if (legendModels && legendModels.length) { ecModel.filterSeries(function (series) { // If in any legend component the status is not selected. // Because in legend series is assumed selected when it is not in the legend data. for (var i = 0; i < legendModels.length; i++) { if (!legendModels[i].isSelected(series.name)) { return false; } } return true; }); } }; // Do not contain scrollable legend, for sake of file size. // Series Filter registerProcessor(legendFilter); ComponentModel.registerSubTypeDefaulter('legend', function () { // Default 'plain' when no type specified. return 'plain'; }); var ScrollableLegendModel = LegendModel.extend({ type: 'legend.scroll', /** * @param {number} scrollDataIndex */ setScrollDataIndex: function (scrollDataIndex) { this.option.scrollDataIndex = scrollDataIndex; }, defaultOption: { scrollDataIndex: 0, pageButtonItemGap: 5, pageButtonGap: null, pageButtonPosition: 'end', // 'start' or 'end' pageFormatter: '{current}/{total}', // If null/undefined, do not show page. pageIcons: { horizontal: ['M0,0L12,-10L12,10z', 'M0,0L-12,-10L-12,10z'], vertical: ['M0,0L20,0L10,-20z', 'M0,0L20,0L10,20z'] }, pageIconColor: '#2f4554', pageIconInactiveColor: '#aaa', pageIconSize: 15, // Can be [10, 3], which represents [width, height] pageTextStyle: { color: '#333' }, animationDurationUpdate: 800 }, /** * @override */ init: function (option, parentModel, ecModel, extraOpt) { var inputPositionParams = getLayoutParams(option); ScrollableLegendModel.superCall(this, 'init', option, parentModel, ecModel, extraOpt); mergeAndNormalizeLayoutParams(this, option, inputPositionParams); }, /** * @override */ mergeOption: function (option, extraOpt) { ScrollableLegendModel.superCall(this, 'mergeOption', option, extraOpt); mergeAndNormalizeLayoutParams(this, this.option, option); }, getOrient: function () { return this.get('orient') === 'vertical' ? {index: 1, name: 'vertical'} : {index: 0, name: 'horizontal'}; } }); // Do not `ignoreSize` to enable setting {left: 10, right: 10}. function mergeAndNormalizeLayoutParams(legendModel, target, raw) { var orient = legendModel.getOrient(); var ignoreSize = [1, 1]; ignoreSize[orient.index] = 0; mergeLayoutParam(target, raw, { type: 'box', ignoreSize: ignoreSize }); } /** * Separate legend and scrollable legend to reduce package size. */ var Group$4 = Group; var WH$1 = ['width', 'height']; var XY$1 = ['x', 'y']; var ScrollableLegendView = LegendView.extend({ type: 'legend.scroll', newlineDisabled: true, init: function () { ScrollableLegendView.superCall(this, 'init'); /** * @private * @type {number} For `scroll`. */ this._currentIndex = 0; /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._containerGroup = new Group$4()); this._containerGroup.add(this.getContentGroup()); /** * @private * @type {module:zrender/container/Group} */ this.group.add(this._controllerGroup = new Group$4()); /** * * @private */ this._showController; }, /** * @override */ resetInner: function () { ScrollableLegendView.superCall(this, 'resetInner'); this._controllerGroup.removeAll(); this._containerGroup.removeClipPath(); this._containerGroup.__rectSize = null; }, /** * @override */ renderInner: function (itemAlign, legendModel, ecModel, api) { var me = this; // Render content items. ScrollableLegendView.superCall(this, 'renderInner', itemAlign, legendModel, ecModel, api); var controllerGroup = this._controllerGroup; var pageIconSize = legendModel.get('pageIconSize', true); if (!isArray(pageIconSize)) { pageIconSize = [pageIconSize, pageIconSize]; } createPageButton('pagePrev', 0); var pageTextStyleModel = legendModel.getModel('pageTextStyle'); controllerGroup.add(new Text({ name: 'pageText', style: { textFill: pageTextStyleModel.getTextColor(), font: pageTextStyleModel.getFont(), textVerticalAlign: 'middle', textAlign: 'center' }, silent: true })); createPageButton('pageNext', 1); function createPageButton(name, iconIdx) { var pageDataIndexName = name + 'DataIndex'; var icon = createIcon( legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx], { // Buttons will be created in each render, so we do not need // to worry about avoiding using legendModel kept in scope. onclick: bind( me._pageGo, me, pageDataIndexName, legendModel, api ) }, { x: -pageIconSize[0] / 2, y: -pageIconSize[1] / 2, width: pageIconSize[0], height: pageIconSize[1] } ); icon.name = name; controllerGroup.add(icon); } }, /** * @override */ layoutInner: function (legendModel, itemAlign, maxSize) { var contentGroup = this.getContentGroup(); var containerGroup = this._containerGroup; var controllerGroup = this._controllerGroup; var orientIdx = legendModel.getOrient().index; var wh = WH$1[orientIdx]; var hw = WH$1[1 - orientIdx]; var yx = XY$1[1 - orientIdx]; // Place items in contentGroup. box( legendModel.get('orient'), contentGroup, legendModel.get('itemGap'), !orientIdx ? null : maxSize.width, orientIdx ? null : maxSize.height ); box( // Buttons in controller are layout always horizontally. 'horizontal', controllerGroup, legendModel.get('pageButtonItemGap', true) ); var contentRect = contentGroup.getBoundingRect(); var controllerRect = controllerGroup.getBoundingRect(); var showController = this._showController = contentRect[wh] > maxSize[wh]; var contentPos = [-contentRect.x, -contentRect.y]; // Remain contentPos when scroll animation perfroming. contentPos[orientIdx] = contentGroup.position[orientIdx]; // Layout container group based on 0. var containerPos = [0, 0]; var controllerPos = [-controllerRect.x, -controllerRect.y]; var pageButtonGap = retrieve2( legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true) ); // Place containerGroup and controllerGroup and contentGroup. if (showController) { var pageButtonPosition = legendModel.get('pageButtonPosition', true); // controller is on the right / bottom. if (pageButtonPosition === 'end') { controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh]; } // controller is on the left / top. else { containerPos[orientIdx] += controllerRect[wh] + pageButtonGap; } } // Always align controller to content as 'middle'. controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2; contentGroup.attr('position', contentPos); containerGroup.attr('position', containerPos); controllerGroup.attr('position', controllerPos); // Calculate `mainRect` and set `clipPath`. // mainRect should not be calculated by `this.group.getBoundingRect()` // for sake of the overflow. var mainRect = this.group.getBoundingRect(); var mainRect = {x: 0, y: 0}; // Consider content may be overflow (should be clipped). mainRect[wh] = showController ? maxSize[wh] : contentRect[wh]; mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]); // `containerRect[yx] + containerPos[1 - orientIdx]` is 0. mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]); containerGroup.__rectSize = maxSize[wh]; if (showController) { var clipShape = {x: 0, y: 0}; clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0); clipShape[hw] = mainRect[hw]; containerGroup.setClipPath(new Rect({shape: clipShape})); // Consider content may be larger than container, container rect // can not be obtained from `containerGroup.getBoundingRect()`. containerGroup.__rectSize = clipShape[wh]; } else { // Do not remove or ignore controller. Keep them set as place holders. controllerGroup.eachChild(function (child) { child.attr({invisible: true, silent: true}); }); } // Content translate animation. var pageInfo = this._getPageInfo(legendModel); pageInfo.pageIndex != null && updateProps( contentGroup, {position: pageInfo.contentPosition}, // When switch from "show controller" to "not show controller", view should be // updated immediately without animation, otherwise causes weird efffect. showController ? legendModel : false ); this._updatePageInfoView(legendModel, pageInfo); return mainRect; }, _pageGo: function (to, legendModel, api) { var scrollDataIndex = this._getPageInfo(legendModel)[to]; scrollDataIndex != null && api.dispatchAction({ type: 'legendScroll', scrollDataIndex: scrollDataIndex, legendId: legendModel.id }); }, _updatePageInfoView: function (legendModel, pageInfo) { var controllerGroup = this._controllerGroup; each$1(['pagePrev', 'pageNext'], function (name) { var canJump = pageInfo[name + 'DataIndex'] != null; var icon = controllerGroup.childOfName(name); if (icon) { icon.setStyle( 'fill', canJump ? legendModel.get('pageIconColor', true) : legendModel.get('pageIconInactiveColor', true) ); icon.cursor = canJump ? 'pointer' : 'default'; } }); var pageText = controllerGroup.childOfName('pageText'); var pageFormatter = legendModel.get('pageFormatter'); var pageIndex = pageInfo.pageIndex; var current = pageIndex != null ? pageIndex + 1 : 0; var total = pageInfo.pageCount; pageText && pageFormatter && pageText.setStyle( 'text', isString(pageFormatter) ? pageFormatter.replace('{current}', current).replace('{total}', total) : pageFormatter({current: current, total: total}) ); }, /** * @param {module:echarts/model/Model} legendModel * @return {Object} { * contentPosition: Array.<number>, null when data item not found. * pageIndex: number, null when data item not found. * pageCount: number, always be a number, can be 0. * pagePrevDataIndex: number, null when no next page. * pageNextDataIndex: number, null when no previous page. * } */ _getPageInfo: function (legendModel) { // Align left or top by the current dataIndex. var currDataIndex = legendModel.get('scrollDataIndex', true); var contentGroup = this.getContentGroup(); var contentRect = contentGroup.getBoundingRect(); var containerRectSize = this._containerGroup.__rectSize; var orientIdx = legendModel.getOrient().index; var wh = WH$1[orientIdx]; var hw = WH$1[1 - orientIdx]; var xy = XY$1[orientIdx]; var contentPos = contentGroup.position.slice(); var pageIndex; var pagePrevDataIndex; var pageNextDataIndex; var targetItemGroup; if (this._showController) { contentGroup.eachChild(function (child) { if (child.__legendDataIndex === currDataIndex) { targetItemGroup = child; } }); } else { targetItemGroup = contentGroup.childAt(0); } var pageCount = containerRectSize ? Math.ceil(contentRect[wh] / containerRectSize) : 0; if (targetItemGroup) { var itemRect = targetItemGroup.getBoundingRect(); var itemLoc = targetItemGroup.position[orientIdx] + itemRect[xy]; contentPos[orientIdx] = -itemLoc - contentRect[xy]; pageIndex = Math.floor( pageCount * (itemLoc + itemRect[xy] + containerRectSize / 2) / contentRect[wh] ); pageIndex = (contentRect[wh] && pageCount) ? Math.max(0, Math.min(pageCount - 1, pageIndex)) : -1; var winRect = {x: 0, y: 0}; winRect[wh] = containerRectSize; winRect[hw] = contentRect[hw]; winRect[xy] = -contentPos[orientIdx] - contentRect[xy]; var startIdx; var children = contentGroup.children(); contentGroup.eachChild(function (child, index) { var itemRect = getItemRect(child); if (itemRect.intersect(winRect)) { startIdx == null && (startIdx = index); // It is user-friendly that the last item shown in the // current window is shown at the begining of next window. pageNextDataIndex = child.__legendDataIndex; } // If the last item is shown entirely, no next page. if (index === children.length - 1 && itemRect[xy] + itemRect[wh] <= winRect[xy] + winRect[wh] ) { pageNextDataIndex = null; } }); // Always align based on the left/top most item, so the left/top most // item in the previous window is needed to be found here. if (startIdx != null) { var startItem = children[startIdx]; var startRect = getItemRect(startItem); winRect[xy] = startRect[xy] + startRect[wh] - winRect[wh]; // If the first item is shown entirely, no previous page. if (startIdx <= 0 && startRect[xy] >= winRect[xy]) { pagePrevDataIndex = null; } else { while (startIdx > 0 && getItemRect(children[startIdx - 1]).intersect(winRect)) { startIdx--; } pagePrevDataIndex = children[startIdx].__legendDataIndex; } } } return { contentPosition: contentPos, pageIndex: pageIndex, pageCount: pageCount, pagePrevDataIndex: pagePrevDataIndex, pageNextDataIndex: pageNextDataIndex }; function getItemRect(el) { var itemRect = el.getBoundingRect().clone(); itemRect[xy] += el.position[orientIdx]; return itemRect; } } }); /** * @event legendScroll * @type {Object} * @property {string} type 'legendScroll' * @property {string} scrollDataIndex */ registerAction( 'legendScroll', 'legendscroll', function (payload, ecModel) { var scrollDataIndex = payload.scrollDataIndex; scrollDataIndex != null && ecModel.eachComponent( {mainType: 'legend', subType: 'scroll', query: payload}, function (legendModel) { legendModel.setScrollDataIndex(scrollDataIndex); } ); } ); /** * Legend component entry file8 */ extendComponentModel({ type: 'tooltip', dependencies: ['axisPointer'], defaultOption: { zlevel: 0, z: 8, show: true, // tooltip主体内容 showContent: true, // 'trigger' only works on coordinate system. // 'item' | 'axis' | 'none' trigger: 'item', // 'click' | 'mousemove' | 'none' triggerOn: 'mousemove|click', alwaysShowContent: false, displayMode: 'single', // 'single' | 'multipleByCoordSys' // 位置 {Array} | {Function} // position: null // Consider triggered from axisPointer handle, verticalAlign should be 'middle' // align: null, // verticalAlign: null, // 是否约束 content 在 viewRect 中。默认 false 是为了兼容以前版本。 confine: false, // 内容格式器:{string}(Template) ¦ {Function} // formatter: null showDelay: 0, // 隐藏延迟,单位ms hideDelay: 100, // 动画变换时间,单位s transitionDuration: 0.4, enterable: false, // 提示背景颜色,默认为透明度为0.7的黑色 backgroundColor: 'rgba(50,50,50,0.7)', // 提示边框颜色 borderColor: '#333', // 提示边框圆角,单位px,默认为4 borderRadius: 4, // 提示边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 提示内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // Extra css text extraCssText: '', // 坐标轴指示器,坐标轴触发有效 axisPointer: { // 默认为直线 // 可选为:'line' | 'shadow' | 'cross' type: 'line', // type 为 line 的时候有效,指定 tooltip line 所在的轴,可选 // 可选 'x' | 'y' | 'angle' | 'radius' | 'auto' // 默认 'auto',会选择类型为 category 的轴,对于双数值轴,笛卡尔坐标系会默认选择 x 轴 // 极坐标系会默认选择 angle 轴 axis: 'auto', animation: 'auto', animationDurationUpdate: 200, animationEasingUpdate: 'exponentialOut', crossStyle: { color: '#999', width: 1, type: 'dashed', // TODO formatter textStyle: {} } // lineStyle and shadowStyle should not be specified here, // otherwise it will always override those styles on option.axisPointer. }, textStyle: { color: '#fff', fontSize: 14 } } }); var each$19 = each$1; var toCamelCase$1 = toCamelCase; var vendors = ['', '-webkit-', '-moz-', '-o-']; var gCssText = 'position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;'; /** * @param {number} duration * @return {string} * @inner */ function assembleTransition(duration) { var transitionCurve = 'cubic-bezier(0.23, 1, 0.32, 1)'; var transitionText = 'left ' + duration + 's ' + transitionCurve + ',' + 'top ' + duration + 's ' + transitionCurve; return map(vendors, function (vendorPrefix) { return vendorPrefix + 'transition:' + transitionText; }).join(';'); } /** * @param {Object} textStyle * @return {string} * @inner */ function assembleFont(textStyleModel) { var cssText = []; var fontSize = textStyleModel.get('fontSize'); var color = textStyleModel.getTextColor(); color && cssText.push('color:' + color); cssText.push('font:' + textStyleModel.getFont()); fontSize && cssText.push('line-height:' + Math.round(fontSize * 3 / 2) + 'px'); each$19(['decoration', 'align'], function (name) { var val = textStyleModel.get(name); val && cssText.push('text-' + name + ':' + val); }); return cssText.join(';'); } /** * @param {Object} tooltipModel * @return {string} * @inner */ function assembleCssText(tooltipModel) { var cssText = []; var transitionDuration = tooltipModel.get('transitionDuration'); var backgroundColor = tooltipModel.get('backgroundColor'); var textStyleModel = tooltipModel.getModel('textStyle'); var padding = tooltipModel.get('padding'); // Animation transition. Do not animate when transitionDuration is 0. transitionDuration && cssText.push(assembleTransition(transitionDuration)); if (backgroundColor) { if (env$1.canvasSupported) { cssText.push('background-Color:' + backgroundColor); } else { // for ie cssText.push( 'background-Color:#' + toHex(backgroundColor) ); cssText.push('filter:alpha(opacity=70)'); } } // Border style each$19(['width', 'color', 'radius'], function (name) { var borderName = 'border-' + name; var camelCase = toCamelCase$1(borderName); var val = tooltipModel.get(camelCase); val != null && cssText.push(borderName + ':' + val + (name === 'color' ? '' : 'px')); }); // Text style cssText.push(assembleFont(textStyleModel)); // Padding if (padding != null) { cssText.push('padding:' + normalizeCssArray$1(padding).join('px ') + 'px'); } return cssText.join(';') + ';'; } /** * @alias module:echarts/component/tooltip/TooltipContent * @constructor */ function TooltipContent(container, api) { if (env$1.wxa) { return null; } var el = document.createElement('div'); var zr = this._zr = api.getZr(); this.el = el; this._x = api.getWidth() / 2; this._y = api.getHeight() / 2; container.appendChild(el); this._container = container; this._show = false; /** * @private */ this._hideTimeout; var self = this; el.onmouseenter = function () { // clear the timeout in hideLater and keep showing tooltip if (self._enterable) { clearTimeout(self._hideTimeout); self._show = true; } self._inContent = true; }; el.onmousemove = function (e) { e = e || window.event; if (!self._enterable) { // Try trigger zrender event to avoid mouse // in and out shape too frequently var handler = zr.handler; normalizeEvent(container, e, true); handler.dispatch('mousemove', e); } }; el.onmouseleave = function () { if (self._enterable) { if (self._show) { self.hideLater(self._hideDelay); } } self._inContent = false; }; } TooltipContent.prototype = { constructor: TooltipContent, /** * @private * @type {boolean} */ _enterable: true, /** * Update when tooltip is rendered */ update: function () { // FIXME // Move this logic to ec main? var container = this._container; var stl = container.currentStyle || document.defaultView.getComputedStyle(container); var domStyle = container.style; if (domStyle.position !== 'absolute' && stl.position !== 'absolute') { domStyle.position = 'relative'; } // Hide the tooltip // PENDING // this.hide(); }, show: function (tooltipModel) { clearTimeout(this._hideTimeout); var el = this.el; el.style.cssText = gCssText + assembleCssText(tooltipModel) // http://stackoverflow.com/questions/21125587/css3-transition-not-working-in-chrome-anymore + ';left:' + this._x + 'px;top:' + this._y + 'px;' + (tooltipModel.get('extraCssText') || ''); el.style.display = el.innerHTML ? 'block' : 'none'; this._show = true; }, setContent: function (content) { this.el.innerHTML = content == null ? '' : content; }, setEnterable: function (enterable) { this._enterable = enterable; }, getSize: function () { var el = this.el; return [el.clientWidth, el.clientHeight]; }, moveTo: function (x, y) { // xy should be based on canvas root. But tooltipContent is // the sibling of canvas root. So padding of ec container // should be considered here. var zr = this._zr; var viewportRootOffset; if (zr && zr.painter && (viewportRootOffset = zr.painter.getViewportRootOffset())) { x += viewportRootOffset.offsetLeft; y += viewportRootOffset.offsetTop; } var style = this.el.style; style.left = x + 'px'; style.top = y + 'px'; this._x = x; this._y = y; }, hide: function () { this.el.style.display = 'none'; this._show = false; }, hideLater: function (time) { if (this._show && !(this._inContent && this._enterable)) { if (time) { this._hideDelay = time; // Set show false to avoid invoke hideLater mutiple times this._show = false; this._hideTimeout = setTimeout(bind(this.hide, this), time); } else { this.hide(); } } }, isShow: function () { return this._show; } }; var bind$3 = bind; var each$18 = each$1; var parsePercent$2 = parsePercent$1; var proxyRect = new Rect({ shape: {x: -1, y: -1, width: 2, height: 2} }); extendComponentView({ type: 'tooltip', init: function (ecModel, api) { if (env$1.node) { return; } var tooltipContent = new TooltipContent(api.getDom(), api); this._tooltipContent = tooltipContent; }, render: function (tooltipModel, ecModel, api) { if (env$1.node || env$1.wxa) { return; } // Reset this.group.removeAll(); /** * @private * @type {module:echarts/component/tooltip/TooltipModel} */ this._tooltipModel = tooltipModel; /** * @private * @type {module:echarts/model/Global} */ this._ecModel = ecModel; /** * @private * @type {module:echarts/ExtensionAPI} */ this._api = api; /** * Should be cleaned when render. * @private * @type {Array.<Array.<Object>>} */ this._lastDataByCoordSys = null; /** * @private * @type {boolean} */ this._alwaysShowContent = tooltipModel.get('alwaysShowContent'); var tooltipContent = this._tooltipContent; tooltipContent.update(); tooltipContent.setEnterable(tooltipModel.get('enterable')); this._initGlobalListener(); this._keepShow(); }, _initGlobalListener: function () { var tooltipModel = this._tooltipModel; var triggerOn = tooltipModel.get('triggerOn'); register( 'itemTooltip', this._api, bind$3(function (currTrigger, e, dispatchAction) { // If 'none', it is not controlled by mouse totally. if (triggerOn !== 'none') { if (triggerOn.indexOf(currTrigger) >= 0) { this._tryShow(e, dispatchAction); } else if (currTrigger === 'leave') { this._hide(dispatchAction); } } }, this) ); }, _keepShow: function () { var tooltipModel = this._tooltipModel; var ecModel = this._ecModel; var api = this._api; // Try to keep the tooltip show when refreshing if (this._lastX != null && this._lastY != null // When user is willing to control tooltip totally using API, // self.manuallyShowTip({x, y}) might cause tooltip hide, // which is not expected. && tooltipModel.get('triggerOn') !== 'none' ) { var self = this; clearTimeout(this._refreshUpdateTimeout); this._refreshUpdateTimeout = setTimeout(function () { // Show tip next tick after other charts are rendered // In case highlight action has wrong result // FIXME self.manuallyShowTip(tooltipModel, ecModel, api, { x: self._lastX, y: self._lastY }); }); } }, /** * Show tip manually by * dispatchAction({ * type: 'showTip', * x: 10, * y: 10 * }); * Or * dispatchAction({ * type: 'showTip', * seriesIndex: 0, * dataIndex or dataIndexInside or name * }); * * TODO Batch */ manuallyShowTip: function (tooltipModel, ecModel, api, payload) { if (payload.from === this.uid || env$1.node) { return; } var dispatchAction = makeDispatchAction$1(payload, api); // Reset ticket this._ticket = ''; // When triggered from axisPointer. var dataByCoordSys = payload.dataByCoordSys; if (payload.tooltip && payload.x != null && payload.y != null) { var el = proxyRect; el.position = [payload.x, payload.y]; el.update(); el.tooltip = payload.tooltip; // Manually show tooltip while view is not using zrender elements. this._tryShow({ offsetX: payload.x, offsetY: payload.y, target: el }, dispatchAction); } else if (dataByCoordSys) { this._tryShow({ offsetX: payload.x, offsetY: payload.y, position: payload.position, event: {}, dataByCoordSys: payload.dataByCoordSys, tooltipOption: payload.tooltipOption }, dispatchAction); } else if (payload.seriesIndex != null) { if (this._manuallyAxisShowTip(tooltipModel, ecModel, api, payload)) { return; } var pointInfo = findPointFromSeries(payload, ecModel); var cx = pointInfo.point[0]; var cy = pointInfo.point[1]; if (cx != null && cy != null) { this._tryShow({ offsetX: cx, offsetY: cy, position: payload.position, target: pointInfo.el, event: {} }, dispatchAction); } } else if (payload.x != null && payload.y != null) { // FIXME // should wrap dispatchAction like `axisPointer/globalListener` ? api.dispatchAction({ type: 'updateAxisPointer', x: payload.x, y: payload.y }); this._tryShow({ offsetX: payload.x, offsetY: payload.y, position: payload.position, target: api.getZr().findHover(payload.x, payload.y).target, event: {} }, dispatchAction); } }, manuallyHideTip: function (tooltipModel, ecModel, api, payload) { var tooltipContent = this._tooltipContent; if (!this._alwaysShowContent && this._tooltipModel) { tooltipContent.hideLater(this._tooltipModel.get('hideDelay')); } this._lastX = this._lastY = null; if (payload.from !== this.uid) { this._hide(makeDispatchAction$1(payload, api)); } }, // Be compatible with previous design, that is, when tooltip.type is 'axis' and // dispatchAction 'showTip' with seriesIndex and dataIndex will trigger axis pointer // and tooltip. _manuallyAxisShowTip: function (tooltipModel, ecModel, api, payload) { var seriesIndex = payload.seriesIndex; var dataIndex = payload.dataIndex; var coordSysAxesInfo = ecModel.getComponent('axisPointer').coordSysAxesInfo; if (seriesIndex == null || dataIndex == null || coordSysAxesInfo == null) { return; } var seriesModel = ecModel.getSeriesByIndex(seriesIndex); if (!seriesModel) { return; } var data = seriesModel.getData(); var tooltipModel = buildTooltipModel([ data.getItemModel(dataIndex), seriesModel, (seriesModel.coordinateSystem || {}).model, tooltipModel ]); if (tooltipModel.get('trigger') !== 'axis') { return; } api.dispatchAction({ type: 'updateAxisPointer', seriesIndex: seriesIndex, dataIndex: dataIndex, position: payload.position }); return true; }, _tryShow: function (e, dispatchAction) { var el = e.target; var tooltipModel = this._tooltipModel; if (!tooltipModel) { return; } // Save mouse x, mouse y. So we can try to keep showing the tip if chart is refreshed this._lastX = e.offsetX; this._lastY = e.offsetY; var dataByCoordSys = e.dataByCoordSys; if (dataByCoordSys && dataByCoordSys.length) { this._showAxisTooltip(dataByCoordSys, e); } // Always show item tooltip if mouse is on the element with dataIndex else if (el && el.dataIndex != null) { this._lastDataByCoordSys = null; this._showSeriesItemTooltip(e, el, dispatchAction); } // Tooltip provided directly. Like legend. else if (el && el.tooltip) { this._lastDataByCoordSys = null; this._showComponentItemTooltip(e, el, dispatchAction); } else { this._lastDataByCoordSys = null; this._hide(dispatchAction); } }, _showOrMove: function (tooltipModel, cb) { // showDelay is used in this case: tooltip.enterable is set // as true. User intent to move mouse into tooltip and click // something. `showDelay` makes it easyer to enter the content // but tooltip do not move immediately. var delay = tooltipModel.get('showDelay'); cb = bind(cb, this); clearTimeout(this._showTimout); delay > 0 ? (this._showTimout = setTimeout(cb, delay)) : cb(); }, _showAxisTooltip: function (dataByCoordSys, e) { var ecModel = this._ecModel; var globalTooltipModel = this._tooltipModel; var point = [e.offsetX, e.offsetY]; var singleDefaultHTML = []; var singleParamsList = []; var singleTooltipModel = buildTooltipModel([ e.tooltipOption, globalTooltipModel ]); each$18(dataByCoordSys, function (itemCoordSys) { // var coordParamList = []; // var coordDefaultHTML = []; // var coordTooltipModel = buildTooltipModel([ // e.tooltipOption, // itemCoordSys.tooltipOption, // ecModel.getComponent(itemCoordSys.coordSysMainType, itemCoordSys.coordSysIndex), // globalTooltipModel // ]); // var displayMode = coordTooltipModel.get('displayMode'); // var paramsList = displayMode === 'single' ? singleParamsList : []; each$18(itemCoordSys.dataByAxis, function (item) { var axisModel = ecModel.getComponent(item.axisDim + 'Axis', item.axisIndex); var axisValue = item.value; var seriesDefaultHTML = []; if (!axisModel || axisValue == null) { return; } var valueLabel = getValueLabel( axisValue, axisModel.axis, ecModel, item.seriesDataIndices, item.valueLabelOpt ); each$1(item.seriesDataIndices, function (idxItem) { var series = ecModel.getSeriesByIndex(idxItem.seriesIndex); var dataIndex = idxItem.dataIndexInside; var dataParams = series && series.getDataParams(dataIndex); dataParams.axisDim = item.axisDim; dataParams.axisIndex = item.axisIndex; dataParams.axisType = item.axisType; dataParams.axisId = item.axisId; dataParams.axisValue = getAxisRawValue(axisModel.axis, axisValue); dataParams.axisValueLabel = valueLabel; if (dataParams) { singleParamsList.push(dataParams); seriesDefaultHTML.push(series.formatTooltip(dataIndex, true)); } }); // Default tooltip content // FIXME // (1) shold be the first data which has name? // (2) themeRiver, firstDataIndex is array, and first line is unnecessary. var firstLine = valueLabel; singleDefaultHTML.push( (firstLine ? encodeHTML(firstLine) + '<br />' : '') + seriesDefaultHTML.join('<br />') ); }); }, this); // In most case, the second axis is shown upper than the first one. singleDefaultHTML.reverse(); singleDefaultHTML = singleDefaultHTML.join('<br /><br />'); var positionExpr = e.position; this._showOrMove(singleTooltipModel, function () { if (this._updateContentNotChangedOnAxis(dataByCoordSys)) { this._updatePosition( singleTooltipModel, positionExpr, point[0], point[1], this._tooltipContent, singleParamsList ); } else { this._showTooltipContent( singleTooltipModel, singleDefaultHTML, singleParamsList, Math.random(), point[0], point[1], positionExpr ); } }); // Do not trigger events here, because this branch only be entered // from dispatchAction. }, _showSeriesItemTooltip: function (e, el, dispatchAction) { var ecModel = this._ecModel; // Use dataModel in element if possible // Used when mouseover on a element like markPoint or edge // In which case, the data is not main data in series. var seriesIndex = el.seriesIndex; var seriesModel = ecModel.getSeriesByIndex(seriesIndex); // For example, graph link. var dataModel = el.dataModel || seriesModel; var dataIndex = el.dataIndex; var dataType = el.dataType; var data = dataModel.getData(); var tooltipModel = buildTooltipModel([ data.getItemModel(dataIndex), dataModel, seriesModel && (seriesModel.coordinateSystem || {}).model, this._tooltipModel ]); var tooltipTrigger = tooltipModel.get('trigger'); if (tooltipTrigger != null && tooltipTrigger !== 'item') { return; } var params = dataModel.getDataParams(dataIndex, dataType); var defaultHtml = dataModel.formatTooltip(dataIndex, false, dataType); var asyncTicket = 'item_' + dataModel.name + '_' + dataIndex; this._showOrMove(tooltipModel, function () { this._showTooltipContent( tooltipModel, defaultHtml, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target ); }); // FIXME // duplicated showtip if manuallyShowTip is called from dispatchAction. dispatchAction({ type: 'showTip', dataIndexInside: dataIndex, dataIndex: data.getRawIndex(dataIndex), seriesIndex: seriesIndex, from: this.uid }); }, _showComponentItemTooltip: function (e, el, dispatchAction) { var tooltipOpt = el.tooltip; if (typeof tooltipOpt === 'string') { var content = tooltipOpt; tooltipOpt = { content: content, // Fixed formatter formatter: content }; } var subTooltipModel = new Model(tooltipOpt, this._tooltipModel, this._ecModel); var defaultHtml = subTooltipModel.get('content'); var asyncTicket = Math.random(); // Do not check whether `trigger` is 'none' here, because `trigger` // only works on cooridinate system. In fact, we have not found case // that requires setting `trigger` nothing on component yet. this._showOrMove(subTooltipModel, function () { this._showTooltipContent( subTooltipModel, defaultHtml, subTooltipModel.get('formatterParams') || {}, asyncTicket, e.offsetX, e.offsetY, e.position, el ); }); // If not dispatch showTip, tip may be hide triggered by axis. dispatchAction({ type: 'showTip', from: this.uid }); }, _showTooltipContent: function ( tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el ) { // Reset ticket this._ticket = ''; if (!tooltipModel.get('showContent') || !tooltipModel.get('show')) { return; } var tooltipContent = this._tooltipContent; var formatter = tooltipModel.get('formatter'); positionExpr = positionExpr || tooltipModel.get('position'); var html = defaultHtml; if (formatter && typeof formatter === 'string') { html = formatTpl(formatter, params, true); } else if (typeof formatter === 'function') { var callback = bind$3(function (cbTicket, html) { if (cbTicket === this._ticket) { tooltipContent.setContent(html); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); } }, this); this._ticket = asyncTicket; html = formatter(params, asyncTicket, callback); } tooltipContent.setContent(html); tooltipContent.show(tooltipModel); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); }, /** * @param {string|Function|Array.<number>|Object} positionExpr * @param {number} x Mouse x * @param {number} y Mouse y * @param {boolean} confine Whether confine tooltip content in view rect. * @param {Object|<Array.<Object>} params * @param {module:zrender/Element} el target element * @param {module:echarts/ExtensionAPI} api * @return {Array.<number>} */ _updatePosition: function (tooltipModel, positionExpr, x, y, content, params, el) { var viewWidth = this._api.getWidth(); var viewHeight = this._api.getHeight(); positionExpr = positionExpr || tooltipModel.get('position'); var contentSize = content.getSize(); var align = tooltipModel.get('align'); var vAlign = tooltipModel.get('verticalAlign'); var rect = el && el.getBoundingRect().clone(); el && rect.applyTransform(el.transform); if (typeof positionExpr === 'function') { // Callback of position can be an array or a string specify the position positionExpr = positionExpr([x, y], params, content.el, rect, { viewSize: [viewWidth, viewHeight], contentSize: contentSize.slice() }); } if (isArray(positionExpr)) { x = parsePercent$2(positionExpr[0], viewWidth); y = parsePercent$2(positionExpr[1], viewHeight); } else if (isObject$1(positionExpr)) { positionExpr.width = contentSize[0]; positionExpr.height = contentSize[1]; var layoutRect = getLayoutRect( positionExpr, {width: viewWidth, height: viewHeight} ); x = layoutRect.x; y = layoutRect.y; align = null; // When positionExpr is left/top/right/bottom, // align and verticalAlign will not work. vAlign = null; } // Specify tooltip position by string 'top' 'bottom' 'left' 'right' around graphic element else if (typeof positionExpr === 'string' && el) { var pos = calcTooltipPosition( positionExpr, rect, contentSize ); x = pos[0]; y = pos[1]; } else { var pos = refixTooltipPosition( x, y, content.el, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20 ); x = pos[0]; y = pos[1]; } align && (x -= isCenterAlign(align) ? contentSize[0] / 2 : align === 'right' ? contentSize[0] : 0); vAlign && (y -= isCenterAlign(vAlign) ? contentSize[1] / 2 : vAlign === 'bottom' ? contentSize[1] : 0); if (tooltipModel.get('confine')) { var pos = confineTooltipPosition( x, y, content.el, viewWidth, viewHeight ); x = pos[0]; y = pos[1]; } content.moveTo(x, y); }, // FIXME // Should we remove this but leave this to user? _updateContentNotChangedOnAxis: function (dataByCoordSys) { var lastCoordSys = this._lastDataByCoordSys; var contentNotChanged = !!lastCoordSys && lastCoordSys.length === dataByCoordSys.length; contentNotChanged && each$18(lastCoordSys, function (lastItemCoordSys, indexCoordSys) { var lastDataByAxis = lastItemCoordSys.dataByAxis || {}; var thisItemCoordSys = dataByCoordSys[indexCoordSys] || {}; var thisDataByAxis = thisItemCoordSys.dataByAxis || []; contentNotChanged &= lastDataByAxis.length === thisDataByAxis.length; contentNotChanged && each$18(lastDataByAxis, function (lastItem, indexAxis) { var thisItem = thisDataByAxis[indexAxis] || {}; var lastIndices = lastItem.seriesDataIndices || []; var newIndices = thisItem.seriesDataIndices || []; contentNotChanged &= lastItem.value === thisItem.value && lastItem.axisType === thisItem.axisType && lastItem.axisId === thisItem.axisId && lastIndices.length === newIndices.length; contentNotChanged && each$18(lastIndices, function (lastIdxItem, j) { var newIdxItem = newIndices[j]; contentNotChanged &= lastIdxItem.seriesIndex === newIdxItem.seriesIndex && lastIdxItem.dataIndex === newIdxItem.dataIndex; }); }); }); this._lastDataByCoordSys = dataByCoordSys; return !!contentNotChanged; }, _hide: function (dispatchAction) { // Do not directly hideLater here, because this behavior may be prevented // in dispatchAction when showTip is dispatched. // FIXME // duplicated hideTip if manuallyHideTip is called from dispatchAction. this._lastDataByCoordSys = null; dispatchAction({ type: 'hideTip', from: this.uid }); }, dispose: function (ecModel, api) { if (env$1.node) { return; } this._tooltipContent.hide(); unregister('itemTooltip', api); } }); /** * @param {Array.<Object|module:echarts/model/Model>} modelCascade * From top to bottom. (the last one should be globalTooltipModel); */ function buildTooltipModel(modelCascade) { var resultModel = modelCascade.pop(); while (modelCascade.length) { var tooltipOpt = modelCascade.pop(); if (tooltipOpt) { if (Model.isInstance(tooltipOpt)) { tooltipOpt = tooltipOpt.get('tooltip', true); } // In each data item tooltip can be simply write: // { // value: 10, // tooltip: 'Something you need to know' // } if (typeof tooltipOpt === 'string') { tooltipOpt = {formatter: tooltipOpt}; } resultModel = new Model(tooltipOpt, resultModel, resultModel.ecModel); } } return resultModel; } function makeDispatchAction$1(payload, api) { return payload.dispatchAction || bind(api.dispatchAction, api); } function refixTooltipPosition(x, y, el, viewWidth, viewHeight, gapH, gapV) { var size = getOuterSize(el); var width = size.width; var height = size.height; if (gapH != null) { if (x + width + gapH > viewWidth) { x -= width + gapH; } else { x += gapH; } } if (gapV != null) { if (y + height + gapV > viewHeight) { y -= height + gapV; } else { y += gapV; } } return [x, y]; } function confineTooltipPosition(x, y, el, viewWidth, viewHeight) { var size = getOuterSize(el); var width = size.width; var height = size.height; x = Math.min(x + width, viewWidth) - width; y = Math.min(y + height, viewHeight) - height; x = Math.max(x, 0); y = Math.max(y, 0); return [x, y]; } function getOuterSize(el) { var width = el.clientWidth; var height = el.clientHeight; // Consider browser compatibility. // IE8 does not support getComputedStyle. if (document.defaultView && document.defaultView.getComputedStyle) { var stl = document.defaultView.getComputedStyle(el); if (stl) { width += parseInt(stl.paddingLeft, 10) + parseInt(stl.paddingRight, 10) + parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10); height += parseInt(stl.paddingTop, 10) + parseInt(stl.paddingBottom, 10) + parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10); } } return {width: width, height: height}; } function calcTooltipPosition(position, rect, contentSize) { var domWidth = contentSize[0]; var domHeight = contentSize[1]; var gap = 5; var x = 0; var y = 0; var rectWidth = rect.width; var rectHeight = rect.height; switch (position) { case 'inside': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y + rectHeight / 2 - domHeight / 2; break; case 'top': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y - domHeight - gap; break; case 'bottom': x = rect.x + rectWidth / 2 - domWidth / 2; y = rect.y + rectHeight + gap; break; case 'left': x = rect.x - domWidth - gap; y = rect.y + rectHeight / 2 - domHeight / 2; break; case 'right': x = rect.x + rectWidth + gap; y = rect.y + rectHeight / 2 - domHeight / 2; } return [x, y]; } function isCenterAlign(align) { return align === 'center' || align === 'middle'; } // FIXME Better way to pack data in graphic element /** * @action * @property {string} type * @property {number} seriesIndex * @property {number} dataIndex * @property {number} [x] * @property {number} [y] */ registerAction( { type: 'showTip', event: 'showTip', update: 'tooltip:manuallyShowTip' }, // noop function () {} ); registerAction( { type: 'hideTip', event: 'hideTip', update: 'tooltip:manuallyHideTip' }, // noop function () {} ); function getSeriesStackId$1(seriesModel) { return seriesModel.get('stack') || '__ec_stack_' + seriesModel.seriesIndex; } function getAxisKey$1(axis) { return axis.dim; } /** * @param {string} seriesType * @param {module:echarts/model/Global} ecModel * @param {module:echarts/ExtensionAPI} api */ function barLayoutPolar(seriesType, ecModel, api) { var width = api.getWidth(); var height = api.getHeight(); var lastStackCoords = {}; var barWidthAndOffset = calRadialBar( filter( ecModel.getSeriesByType(seriesType), function (seriesModel) { return !ecModel.isSeriesFiltered(seriesModel) && seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'polar'; } ) ); ecModel.eachSeriesByType(seriesType, function (seriesModel) { // Check series coordinate, do layout for polar only if (seriesModel.coordinateSystem.type !== 'polar') { return; } var data = seriesModel.getData(); var polar = seriesModel.coordinateSystem; var baseAxis = polar.getBaseAxis(); var stackId = getSeriesStackId$1(seriesModel); var columnLayoutInfo = barWidthAndOffset[getAxisKey$1(baseAxis)][stackId]; var columnOffset = columnLayoutInfo.offset; var columnWidth = columnLayoutInfo.width; var valueAxis = polar.getOtherAxis(baseAxis); var center = seriesModel.get('center') || ['50%', '50%']; var cx = parsePercent$1(center[0], width); var cy = parsePercent$1(center[1], height); var barMinHeight = seriesModel.get('barMinHeight') || 0; var barMinAngle = seriesModel.get('barMinAngle') || 0; lastStackCoords[stackId] = lastStackCoords[stackId] || []; var valueDim = data.mapDimension(valueAxis.dim); var baseDim = data.mapDimension(baseAxis.dim); var stacked = isDimensionStacked(data, valueDim, baseDim); var valueAxisStart = valueAxis.getExtent()[0]; for (var idx = 0, len = data.count(); idx < len; idx++) { var value = data.get(valueDim, idx); var baseValue = data.get(baseDim, idx); if (isNaN(value)) { continue; } var sign = value >= 0 ? 'p' : 'n'; var baseCoord = valueAxisStart; // Because of the barMinHeight, we can not use the value in // stackResultDimension directly. // Only ordinal axis can be stacked. if (stacked) { if (!lastStackCoords[stackId][baseValue]) { lastStackCoords[stackId][baseValue] = { p: valueAxisStart, // Positive stack n: valueAxisStart // Negative stack }; } // Should also consider #4243 baseCoord = lastStackCoords[stackId][baseValue][sign]; } var r0; var r; var startAngle; var endAngle; // radial sector if (valueAxis.dim === 'radius') { var radiusSpan = valueAxis.dataToRadius(value) - valueAxisStart; var angle = baseAxis.dataToAngle(baseValue); if (Math.abs(radiusSpan) < barMinHeight) { radiusSpan = (radiusSpan < 0 ? -1 : 1) * barMinHeight; } r0 = baseCoord; r = baseCoord + radiusSpan; startAngle = angle - columnOffset; endAngle = startAngle - columnWidth; stacked && (lastStackCoords[stackId][baseValue][sign] = r); } // tangential sector else { // angleAxis must be clamped. var angleSpan = valueAxis.dataToAngle(value, true) - valueAxisStart; var radius = baseAxis.dataToRadius(baseValue); if (Math.abs(angleSpan) < barMinAngle) { angleSpan = (angleSpan < 0 ? -1 : 1) * barMinAngle; } r0 = radius + columnOffset; r = r0 + columnWidth; startAngle = baseCoord; endAngle = baseCoord + angleSpan; // if the previous stack is at the end of the ring, // add a round to differentiate it from origin // var extent = angleAxis.getExtent(); // var stackCoord = angle; // if (stackCoord === extent[0] && value > 0) { // stackCoord = extent[1]; // } // else if (stackCoord === extent[1] && value < 0) { // stackCoord = extent[0]; // } stacked && (lastStackCoords[stackId][baseValue][sign] = endAngle); } data.setItemLayout(idx, { cx: cx, cy: cy, r0: r0, r: r, // Consider that positive angle is anti-clockwise, // while positive radian of sector is clockwise startAngle: -startAngle * Math.PI / 180, endAngle: -endAngle * Math.PI / 180 }); } }, this); } /** * Calculate bar width and offset for radial bar charts */ function calRadialBar(barSeries, api) { // Columns info on each category axis. Key is polar name var columnsMap = {}; each$1(barSeries, function (seriesModel, idx) { var data = seriesModel.getData(); var polar = seriesModel.coordinateSystem; var baseAxis = polar.getBaseAxis(); var axisExtent = baseAxis.getExtent(); var bandWidth = baseAxis.type === 'category' ? baseAxis.getBandWidth() : (Math.abs(axisExtent[1] - axisExtent[0]) / data.count()); var columnsOnAxis = columnsMap[getAxisKey$1(baseAxis)] || { bandWidth: bandWidth, remainedWidth: bandWidth, autoWidthCount: 0, categoryGap: '20%', gap: '30%', stacks: {} }; var stacks = columnsOnAxis.stacks; columnsMap[getAxisKey$1(baseAxis)] = columnsOnAxis; var stackId = getSeriesStackId$1(seriesModel); if (!stacks[stackId]) { columnsOnAxis.autoWidthCount++; } stacks[stackId] = stacks[stackId] || { width: 0, maxWidth: 0 }; var barWidth = parsePercent$1( seriesModel.get('barWidth'), bandWidth ); var barMaxWidth = parsePercent$1( seriesModel.get('barMaxWidth'), bandWidth ); var barGap = seriesModel.get('barGap'); var barCategoryGap = seriesModel.get('barCategoryGap'); if (barWidth && !stacks[stackId].width) { barWidth = Math.min(columnsOnAxis.remainedWidth, barWidth); stacks[stackId].width = barWidth; columnsOnAxis.remainedWidth -= barWidth; } barMaxWidth && (stacks[stackId].maxWidth = barMaxWidth); (barGap != null) && (columnsOnAxis.gap = barGap); (barCategoryGap != null) && (columnsOnAxis.categoryGap = barCategoryGap); }); var result = {}; each$1(columnsMap, function (columnsOnAxis, coordSysName) { result[coordSysName] = {}; var stacks = columnsOnAxis.stacks; var bandWidth = columnsOnAxis.bandWidth; var categoryGap = parsePercent$1(columnsOnAxis.categoryGap, bandWidth); var barGapPercent = parsePercent$1(columnsOnAxis.gap, 1); var remainedWidth = columnsOnAxis.remainedWidth; var autoWidthCount = columnsOnAxis.autoWidthCount; var autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent); autoWidth = Math.max(autoWidth, 0); // Find if any auto calculated bar exceeded maxBarWidth each$1(stacks, function (column, stack) { var maxWidth = column.maxWidth; if (maxWidth && maxWidth < autoWidth) { maxWidth = Math.min(maxWidth, remainedWidth); if (column.width) { maxWidth = Math.min(maxWidth, column.width); } remainedWidth -= maxWidth; column.width = maxWidth; autoWidthCount--; } }); // Recalculate width again autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent); autoWidth = Math.max(autoWidth, 0); var widthSum = 0; var lastColumn; each$1(stacks, function (column, idx) { if (!column.width) { column.width = autoWidth; } lastColumn = column; widthSum += column.width * (1 + barGapPercent); }); if (lastColumn) { widthSum -= lastColumn.width * barGapPercent; } var offset = -widthSum / 2; each$1(stacks, function (column, stackId) { result[coordSysName][stackId] = result[coordSysName][stackId] || { offset: offset, width: column.width }; offset += column.width * (1 + barGapPercent); }); }); return result; } function RadiusAxis(scale, radiusExtent) { Axis.call(this, 'radius', scale, radiusExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = 'category'; } RadiusAxis.prototype = { constructor: RadiusAxis, /** * @override */ pointToData: function (point, clamp) { return this.polar.pointToData(point, clamp)[this.dim === 'radius' ? 0 : 1]; }, dataToRadius: Axis.prototype.dataToCoord, radiusToData: Axis.prototype.coordToData }; inherits(RadiusAxis, Axis); function AngleAxis(scale, angleExtent) { angleExtent = angleExtent || [0, 360]; Axis.call(this, 'angle', scale, angleExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = 'category'; } AngleAxis.prototype = { constructor: AngleAxis, /** * @override */ pointToData: function (point, clamp) { return this.polar.pointToData(point, clamp)[this.dim === 'radius' ? 0 : 1]; }, dataToAngle: Axis.prototype.dataToCoord, angleToData: Axis.prototype.coordToData }; inherits(AngleAxis, Axis); /** * @module echarts/coord/polar/Polar */ /** * @alias {module:echarts/coord/polar/Polar} * @constructor * @param {string} name */ var Polar = function (name) { /** * @type {string} */ this.name = name || ''; /** * x of polar center * @type {number} */ this.cx = 0; /** * y of polar center * @type {number} */ this.cy = 0; /** * @type {module:echarts/coord/polar/RadiusAxis} * @private */ this._radiusAxis = new RadiusAxis(); /** * @type {module:echarts/coord/polar/AngleAxis} * @private */ this._angleAxis = new AngleAxis(); this._radiusAxis.polar = this._angleAxis.polar = this; }; Polar.prototype = { type: 'polar', axisPointerEnabled: true, constructor: Polar, /** * @param {Array.<string>} * @readOnly */ dimensions: ['radius', 'angle'], /** * @type {module:echarts/coord/PolarModel} */ model: null, /** * If contain coord * @param {Array.<number>} point * @return {boolean} */ containPoint: function (point) { var coord = this.pointToCoord(point); return this._radiusAxis.contain(coord[0]) && this._angleAxis.contain(coord[1]); }, /** * If contain data * @param {Array.<number>} data * @return {boolean} */ containData: function (data) { return this._radiusAxis.containData(data[0]) && this._angleAxis.containData(data[1]); }, /** * @param {string} dim * @return {module:echarts/coord/polar/AngleAxis|module:echarts/coord/polar/RadiusAxis} */ getAxis: function (dim) { return this['_' + dim + 'Axis']; }, /** * @return {Array.<module:echarts/coord/Axis>} */ getAxes: function () { return [this._radiusAxis, this._angleAxis]; }, /** * Get axes by type of scale * @param {string} scaleType * @return {module:echarts/coord/polar/AngleAxis|module:echarts/coord/polar/RadiusAxis} */ getAxesByScale: function (scaleType) { var axes = []; var angleAxis = this._angleAxis; var radiusAxis = this._radiusAxis; angleAxis.scale.type === scaleType && axes.push(angleAxis); radiusAxis.scale.type === scaleType && axes.push(radiusAxis); return axes; }, /** * @return {module:echarts/coord/polar/AngleAxis} */ getAngleAxis: function () { return this._angleAxis; }, /** * @return {module:echarts/coord/polar/RadiusAxis} */ getRadiusAxis: function () { return this._radiusAxis; }, /** * @param {module:echarts/coord/polar/Axis} * @return {module:echarts/coord/polar/Axis} */ getOtherAxis: function (axis) { var angleAxis = this._angleAxis; return axis === angleAxis ? this._radiusAxis : angleAxis; }, /** * Base axis will be used on stacking. * * @return {module:echarts/coord/polar/Axis} */ getBaseAxis: function () { return this.getAxesByScale('ordinal')[0] || this.getAxesByScale('time')[0] || this.getAngleAxis(); }, /** * @param {string} [dim] 'radius' or 'angle' or 'auto' or null/undefined * @return {Object} {baseAxes: [], otherAxes: []} */ getTooltipAxes: function (dim) { var baseAxis = (dim != null && dim !== 'auto') ? this.getAxis(dim) : this.getBaseAxis(); return { baseAxes: [baseAxis], otherAxes: [this.getOtherAxis(baseAxis)] }; }, /** * Convert a single data item to (x, y) point. * Parameter data is an array which the first element is radius and the second is angle * @param {Array.<number>} data * @param {boolean} [clamp=false] * @return {Array.<number>} */ dataToPoint: function (data, clamp) { return this.coordToPoint([ this._radiusAxis.dataToRadius(data[0], clamp), this._angleAxis.dataToAngle(data[1], clamp) ]); }, /** * Convert a (x, y) point to data * @param {Array.<number>} point * @param {boolean} [clamp=false] * @return {Array.<number>} */ pointToData: function (point, clamp) { var coord = this.pointToCoord(point); return [ this._radiusAxis.radiusToData(coord[0], clamp), this._angleAxis.angleToData(coord[1], clamp) ]; }, /** * Convert a (x, y) point to (radius, angle) coord * @param {Array.<number>} point * @return {Array.<number>} */ pointToCoord: function (point) { var dx = point[0] - this.cx; var dy = point[1] - this.cy; var angleAxis = this.getAngleAxis(); var extent = angleAxis.getExtent(); var minAngle = Math.min(extent[0], extent[1]); var maxAngle = Math.max(extent[0], extent[1]); // Fix fixed extent in polarCreator // FIXME angleAxis.inverse ? (minAngle = maxAngle - 360) : (maxAngle = minAngle + 360); var radius = Math.sqrt(dx * dx + dy * dy); dx /= radius; dy /= radius; var radian = Math.atan2(-dy, dx) / Math.PI * 180; // move to angleExtent var dir = radian < minAngle ? 1 : -1; while (radian < minAngle || radian > maxAngle) { radian += dir * 360; } return [radius, radian]; }, /** * Convert a (radius, angle) coord to (x, y) point * @param {Array.<number>} coord * @return {Array.<number>} */ coordToPoint: function (coord) { var radius = coord[0]; var radian = coord[1] / 180 * Math.PI; var x = Math.cos(radian) * radius + this.cx; // Inverse the y var y = -Math.sin(radian) * radius + this.cy; return [x, y]; } }; var PolarAxisModel = ComponentModel.extend({ type: 'polarAxis', /** * @type {module:echarts/coord/polar/AngleAxis|module:echarts/coord/polar/RadiusAxis} */ axis: null, /** * @override */ getCoordSysModel: function () { return this.ecModel.queryComponents({ mainType: 'polar', index: this.option.polarIndex, id: this.option.polarId })[0]; } }); merge(PolarAxisModel.prototype, axisModelCommonMixin); var polarAxisDefaultExtendedOption = { angle: { // polarIndex: 0, // polarId: '', startAngle: 90, clockwise: true, splitNumber: 12, axisLabel: { rotate: false } }, radius: { // polarIndex: 0, // polarId: '', splitNumber: 5 } }; function getAxisType$3(axisDim, option) { // Default axis with data is category axis return option.type || (option.data ? 'category' : 'value'); } axisModelCreator('angle', PolarAxisModel, getAxisType$3, polarAxisDefaultExtendedOption.angle); axisModelCreator('radius', PolarAxisModel, getAxisType$3, polarAxisDefaultExtendedOption.radius); extendComponentModel({ type: 'polar', dependencies: ['polarAxis', 'angleAxis'], /** * @type {module:echarts/coord/polar/Polar} */ coordinateSystem: null, /** * @param {string} axisType * @return {module:echarts/coord/polar/AxisModel} */ findAxisModel: function (axisType) { var foundAxisModel; var ecModel = this.ecModel; ecModel.eachComponent(axisType, function (axisModel) { if (axisModel.getCoordSysModel() === this) { foundAxisModel = axisModel; } }, this); return foundAxisModel; }, defaultOption: { zlevel: 0, z: 0, center: ['50%', '50%'], radius: '80%' } }); // TODO Axis scale // 依赖 PolarModel 做预处理 /** * Resize method bound to the polar * @param {module:echarts/coord/polar/PolarModel} polarModel * @param {module:echarts/ExtensionAPI} api */ function resizePolar(polar, polarModel, api) { var center = polarModel.get('center'); var width = api.getWidth(); var height = api.getHeight(); polar.cx = parsePercent$1(center[0], width); polar.cy = parsePercent$1(center[1], height); var radiusAxis = polar.getRadiusAxis(); var size = Math.min(width, height) / 2; var radius = parsePercent$1(polarModel.get('radius'), size); radiusAxis.inverse ? radiusAxis.setExtent(radius, 0) : radiusAxis.setExtent(0, radius); } /** * Update polar */ function updatePolarScale(ecModel, api) { var polar = this; var angleAxis = polar.getAngleAxis(); var radiusAxis = polar.getRadiusAxis(); // Reset scale angleAxis.scale.setExtent(Infinity, -Infinity); radiusAxis.scale.setExtent(Infinity, -Infinity); ecModel.eachSeries(function (seriesModel) { if (seriesModel.coordinateSystem === polar) { var data = seriesModel.getData(); each$1(data.mapDimension('radius', true), function (dim) { radiusAxis.scale.unionExtentFromData(data, dim); }); each$1(data.mapDimension('angle', true), function (dim) { angleAxis.scale.unionExtentFromData(data, dim); }); } }); niceScaleExtent(angleAxis.scale, angleAxis.model); niceScaleExtent(radiusAxis.scale, radiusAxis.model); // Fix extent of category angle axis if (angleAxis.type === 'category' && !angleAxis.onBand) { var extent = angleAxis.getExtent(); var diff = 360 / angleAxis.scale.count(); angleAxis.inverse ? (extent[1] += diff) : (extent[1] -= diff); angleAxis.setExtent(extent[0], extent[1]); } } /** * Set common axis properties * @param {module:echarts/coord/polar/AngleAxis|module:echarts/coord/polar/RadiusAxis} * @param {module:echarts/coord/polar/AxisModel} * @inner */ function setAxis(axis, axisModel) { axis.type = axisModel.get('type'); axis.scale = createScaleByModel(axisModel); axis.onBand = axisModel.get('boundaryGap') && axis.type === 'category'; axis.inverse = axisModel.get('inverse'); if (axisModel.mainType === 'angleAxis') { axis.inverse ^= axisModel.get('clockwise'); var startAngle = axisModel.get('startAngle'); axis.setExtent(startAngle, startAngle + (axis.inverse ? -360 : 360)); } // Inject axis instance axisModel.axis = axis; axis.model = axisModel; } var polarCreator = { dimensions: Polar.prototype.dimensions, create: function (ecModel, api) { var polarList = []; ecModel.eachComponent('polar', function (polarModel, idx) { var polar = new Polar(idx); // Inject resize and update method polar.update = updatePolarScale; var radiusAxis = polar.getRadiusAxis(); var angleAxis = polar.getAngleAxis(); var radiusAxisModel = polarModel.findAxisModel('radiusAxis'); var angleAxisModel = polarModel.findAxisModel('angleAxis'); setAxis(radiusAxis, radiusAxisModel); setAxis(angleAxis, angleAxisModel); resizePolar(polar, polarModel, api); polarList.push(polar); polarModel.coordinateSystem = polar; polar.model = polarModel; }); // Inject coordinateSystem to series ecModel.eachSeries(function (seriesModel) { if (seriesModel.get('coordinateSystem') === 'polar') { var polarModel = ecModel.queryComponents({ mainType: 'polar', index: seriesModel.get('polarIndex'), id: seriesModel.get('polarId') })[0]; if (__DEV__) { if (!polarModel) { throw new Error( 'Polar "' + retrieve( seriesModel.get('polarIndex'), seriesModel.get('polarId'), 0 ) + '" not found' ); } } seriesModel.coordinateSystem = polarModel.coordinateSystem; } }); return polarList; } }; CoordinateSystemManager.register('polar', polarCreator); var elementList$1 = ['axisLine', 'axisLabel', 'axisTick', 'splitLine', 'splitArea']; function getAxisLineShape(polar, rExtent, angle) { rExtent[1] > rExtent[0] && (rExtent = rExtent.slice().reverse()); var start = polar.coordToPoint([rExtent[0], angle]); var end = polar.coordToPoint([rExtent[1], angle]); return { x1: start[0], y1: start[1], x2: end[0], y2: end[1] }; } function getRadiusIdx(polar) { var radiusAxis = polar.getRadiusAxis(); return radiusAxis.inverse ? 0 : 1; } AxisView.extend({ type: 'angleAxis', axisPointerClass: 'PolarAxisPointer', render: function (angleAxisModel, ecModel) { this.group.removeAll(); if (!angleAxisModel.get('show')) { return; } var angleAxis = angleAxisModel.axis; var polar = angleAxis.polar; var radiusExtent = polar.getRadiusAxis().getExtent(); var ticksAngles = angleAxis.getTicksCoords(); if (angleAxis.type !== 'category') { // Remove the last tick which will overlap the first tick ticksAngles.pop(); } each$1(elementList$1, function (name) { if (angleAxisModel.get(name +'.show') && (!angleAxis.scale.isBlank() || name === 'axisLine') ) { this['_' + name](angleAxisModel, polar, ticksAngles, radiusExtent); } }, this); }, /** * @private */ _axisLine: function (angleAxisModel, polar, ticksAngles, radiusExtent) { var lineStyleModel = angleAxisModel.getModel('axisLine.lineStyle'); var circle = new Circle({ shape: { cx: polar.cx, cy: polar.cy, r: radiusExtent[getRadiusIdx(polar)] }, style: lineStyleModel.getLineStyle(), z2: 1, silent: true }); circle.style.fill = null; this.group.add(circle); }, /** * @private */ _axisTick: function (angleAxisModel, polar, ticksAngles, radiusExtent) { var tickModel = angleAxisModel.getModel('axisTick'); var tickLen = (tickModel.get('inside') ? -1 : 1) * tickModel.get('length'); var radius = radiusExtent[getRadiusIdx(polar)]; var lines = map(ticksAngles, function (tickAngle) { return new Line({ shape: getAxisLineShape(polar, [radius, radius + tickLen], tickAngle) }); }); this.group.add(mergePath( lines, { style: defaults( tickModel.getModel('lineStyle').getLineStyle(), { stroke: angleAxisModel.get('axisLine.lineStyle.color') } ) } )); }, /** * @private */ _axisLabel: function (angleAxisModel, polar, ticksAngles, radiusExtent) { var axis = angleAxisModel.axis; var categoryData = angleAxisModel.getCategories(); var labelModel = angleAxisModel.getModel('axisLabel'); var labels = angleAxisModel.getFormattedLabels(); var labelMargin = labelModel.get('margin'); var labelsAngles = axis.getLabelsCoords(); // Use length of ticksAngles because it may remove the last tick to avoid overlapping for (var i = 0; i < ticksAngles.length; i++) { var r = radiusExtent[getRadiusIdx(polar)]; var p = polar.coordToPoint([r + labelMargin, labelsAngles[i]]); var cx = polar.cx; var cy = polar.cy; var labelTextAlign = Math.abs(p[0] - cx) / r < 0.3 ? 'center' : (p[0] > cx ? 'left' : 'right'); var labelTextVerticalAlign = Math.abs(p[1] - cy) / r < 0.3 ? 'middle' : (p[1] > cy ? 'top' : 'bottom'); if (categoryData && categoryData[i] && categoryData[i].textStyle) { labelModel = new Model(categoryData[i].textStyle, labelModel, labelModel.ecModel); } var textEl = new Text({silent: true}); this.group.add(textEl); setTextStyle(textEl.style, labelModel, { x: p[0], y: p[1], textFill: labelModel.getTextColor() || angleAxisModel.get('axisLine.lineStyle.color'), text: labels[i], textAlign: labelTextAlign, textVerticalAlign: labelTextVerticalAlign }); } }, /** * @private */ _splitLine: function (angleAxisModel, polar, ticksAngles, radiusExtent) { var splitLineModel = angleAxisModel.getModel('splitLine'); var lineStyleModel = splitLineModel.getModel('lineStyle'); var lineColors = lineStyleModel.get('color'); var lineCount = 0; lineColors = lineColors instanceof Array ? lineColors : [lineColors]; var splitLines = []; for (var i = 0; i < ticksAngles.length; i++) { var colorIndex = (lineCount++) % lineColors.length; splitLines[colorIndex] = splitLines[colorIndex] || []; splitLines[colorIndex].push(new Line({ shape: getAxisLineShape(polar, radiusExtent, ticksAngles[i]) })); } // Simple optimization // Batching the lines if color are the same for (var i = 0; i < splitLines.length; i++) { this.group.add(mergePath(splitLines[i], { style: defaults({ stroke: lineColors[i % lineColors.length] }, lineStyleModel.getLineStyle()), silent: true, z: angleAxisModel.get('z') })); } }, /** * @private */ _splitArea: function (angleAxisModel, polar, ticksAngles, radiusExtent) { var splitAreaModel = angleAxisModel.getModel('splitArea'); var areaStyleModel = splitAreaModel.getModel('areaStyle'); var areaColors = areaStyleModel.get('color'); var lineCount = 0; areaColors = areaColors instanceof Array ? areaColors : [areaColors]; var splitAreas = []; var RADIAN = Math.PI / 180; var prevAngle = -ticksAngles[0] * RADIAN; var r0 = Math.min(radiusExtent[0], radiusExtent[1]); var r1 = Math.max(radiusExtent[0], radiusExtent[1]); var clockwise = angleAxisModel.get('clockwise'); for (var i = 1; i < ticksAngles.length; i++) { var colorIndex = (lineCount++) % areaColors.length; splitAreas[colorIndex] = splitAreas[colorIndex] || []; splitAreas[colorIndex].push(new Sector({ shape: { cx: polar.cx, cy: polar.cy, r0: r0, r: r1, startAngle: prevAngle, endAngle: -ticksAngles[i] * RADIAN, clockwise: clockwise }, silent: true })); prevAngle = -ticksAngles[i] * RADIAN; } // Simple optimization // Batching the lines if color are the same for (var i = 0; i < splitAreas.length; i++) { this.group.add(mergePath(splitAreas[i], { style: defaults({ fill: areaColors[i % areaColors.length] }, areaStyleModel.getAreaStyle()), silent: true })); } } }); var axisBuilderAttrs$3 = [ 'axisLine', 'axisTickLabel', 'axisName' ]; var selfBuilderAttrs$1 = [ 'splitLine', 'splitArea' ]; AxisView.extend({ type: 'radiusAxis', axisPointerClass: 'PolarAxisPointer', render: function (radiusAxisModel, ecModel) { this.group.removeAll(); if (!radiusAxisModel.get('show')) { return; } var radiusAxis = radiusAxisModel.axis; var polar = radiusAxis.polar; var angleAxis = polar.getAngleAxis(); var ticksCoords = radiusAxis.getTicksCoords(); var axisAngle = angleAxis.getExtent()[0]; var radiusExtent = radiusAxis.getExtent(); var layout = layoutAxis(polar, radiusAxisModel, axisAngle); var axisBuilder = new AxisBuilder(radiusAxisModel, layout); each$1(axisBuilderAttrs$3, axisBuilder.add, axisBuilder); this.group.add(axisBuilder.getGroup()); each$1(selfBuilderAttrs$1, function (name) { if (radiusAxisModel.get(name +'.show') && !radiusAxis.scale.isBlank()) { this['_' + name](radiusAxisModel, polar, axisAngle, radiusExtent, ticksCoords); } }, this); }, /** * @private */ _splitLine: function (radiusAxisModel, polar, axisAngle, radiusExtent, ticksCoords) { var splitLineModel = radiusAxisModel.getModel('splitLine'); var lineStyleModel = splitLineModel.getModel('lineStyle'); var lineColors = lineStyleModel.get('color'); var lineCount = 0; lineColors = lineColors instanceof Array ? lineColors : [lineColors]; var splitLines = []; for (var i = 0; i < ticksCoords.length; i++) { var colorIndex = (lineCount++) % lineColors.length; splitLines[colorIndex] = splitLines[colorIndex] || []; splitLines[colorIndex].push(new Circle({ shape: { cx: polar.cx, cy: polar.cy, r: ticksCoords[i] }, silent: true })); } // Simple optimization // Batching the lines if color are the same for (var i = 0; i < splitLines.length; i++) { this.group.add(mergePath(splitLines[i], { style: defaults({ stroke: lineColors[i % lineColors.length], fill: null }, lineStyleModel.getLineStyle()), silent: true })); } }, /** * @private */ _splitArea: function (radiusAxisModel, polar, axisAngle, radiusExtent, ticksCoords) { var splitAreaModel = radiusAxisModel.getModel('splitArea'); var areaStyleModel = splitAreaModel.getModel('areaStyle'); var areaColors = areaStyleModel.get('color'); var lineCount = 0; areaColors = areaColors instanceof Array ? areaColors : [areaColors]; var splitAreas = []; var prevRadius = ticksCoords[0]; for (var i = 1; i < ticksCoords.length; i++) { var colorIndex = (lineCount++) % areaColors.length; splitAreas[colorIndex] = splitAreas[colorIndex] || []; splitAreas[colorIndex].push(new Sector({ shape: { cx: polar.cx, cy: polar.cy, r0: prevRadius, r: ticksCoords[i], startAngle: 0, endAngle: Math.PI * 2 }, silent: true })); prevRadius = ticksCoords[i]; } // Simple optimization // Batching the lines if color are the same for (var i = 0; i < splitAreas.length; i++) { this.group.add(mergePath(splitAreas[i], { style: defaults({ fill: areaColors[i % areaColors.length] }, areaStyleModel.getAreaStyle()), silent: true })); } } }); /** * @inner */ function layoutAxis(polar, radiusAxisModel, axisAngle) { return { position: [polar.cx, polar.cy], rotation: axisAngle / 180 * Math.PI, labelDirection: -1, tickDirection: -1, nameDirection: 1, labelRotate: radiusAxisModel.getModel('axisLabel').get('rotate'), // Over splitLine and splitArea z2: 1 }; } var PolarAxisPointer = BaseAxisPointer.extend({ /** * @override */ makeElOption: function (elOption, value, axisModel, axisPointerModel, api) { var axis = axisModel.axis; if (axis.dim === 'angle') { this.animationThreshold = Math.PI / 18; } var polar = axis.polar; var otherAxis = polar.getOtherAxis(axis); var otherExtent = otherAxis.getExtent(); var coordValue; coordValue = axis['dataTo' + capitalFirst(axis.dim)](value); var axisPointerType = axisPointerModel.get('type'); if (axisPointerType && axisPointerType !== 'none') { var elStyle = buildElStyle(axisPointerModel); var pointerOption = pointerShapeBuilder$2[axisPointerType]( axis, polar, coordValue, otherExtent, elStyle ); pointerOption.style = elStyle; elOption.graphicKey = pointerOption.type; elOption.pointer = pointerOption; } var labelMargin = axisPointerModel.get('label.margin'); var labelPos = getLabelPosition(value, axisModel, axisPointerModel, polar, labelMargin); buildLabelElOption(elOption, axisModel, axisPointerModel, api, labelPos); } // Do not support handle, utill any user requires it. }); function getLabelPosition(value, axisModel, axisPointerModel, polar, labelMargin) { var axis = axisModel.axis; var coord = axis.dataToCoord(value); var axisAngle = polar.getAngleAxis().getExtent()[0]; axisAngle = axisAngle / 180 * Math.PI; var radiusExtent = polar.getRadiusAxis().getExtent(); var position; var align; var verticalAlign; if (axis.dim === 'radius') { var transform = create$1(); rotate(transform, transform, axisAngle); translate(transform, transform, [polar.cx, polar.cy]); position = applyTransform$1([coord, -labelMargin], transform); var labelRotation = axisModel.getModel('axisLabel').get('rotate') || 0; var labelLayout = AxisBuilder.innerTextLayout( axisAngle, labelRotation * Math.PI / 180, -1 ); align = labelLayout.textAlign; verticalAlign = labelLayout.textVerticalAlign; } else { // angle axis var r = radiusExtent[1]; position = polar.coordToPoint([r + labelMargin, coord]); var cx = polar.cx; var cy = polar.cy; align = Math.abs(position[0] - cx) / r < 0.3 ? 'center' : (position[0] > cx ? 'left' : 'right'); verticalAlign = Math.abs(position[1] - cy) / r < 0.3 ? 'middle' : (position[1] > cy ? 'top' : 'bottom'); } return { position: position, align: align, verticalAlign: verticalAlign }; } var pointerShapeBuilder$2 = { line: function (axis, polar, coordValue, otherExtent, elStyle) { return axis.dim === 'angle' ? { type: 'Line', shape: makeLineShape( polar.coordToPoint([otherExtent[0], coordValue]), polar.coordToPoint([otherExtent[1], coordValue]) ) } : { type: 'Circle', shape: { cx: polar.cx, cy: polar.cy, r: coordValue } }; }, shadow: function (axis, polar, coordValue, otherExtent, elStyle) { var bandWidth = axis.getBandWidth(); var radian = Math.PI / 180; return axis.dim === 'angle' ? { type: 'Sector', shape: makeSectorShape( polar.cx, polar.cy, otherExtent[0], otherExtent[1], // In ECharts y is negative if angle is positive (-coordValue - bandWidth / 2) * radian, (-coordValue + bandWidth / 2) * radian ) } : { type: 'Sector', shape: makeSectorShape( polar.cx, polar.cy, coordValue - bandWidth / 2, coordValue + bandWidth / 2, 0, Math.PI * 2 ) }; } }; AxisView.registerAxisPointerClass('PolarAxisPointer', PolarAxisPointer); // For reducing size of echarts.min, barLayoutPolar is required by polar. registerLayout(curry(barLayoutPolar, 'bar')); // Polar view extendComponentView({ type: 'polar' }); var GeoModel = ComponentModel.extend({ type: 'geo', /** * @type {module:echarts/coord/geo/Geo} */ coordinateSystem: null, layoutMode: 'box', init: function (option) { ComponentModel.prototype.init.apply(this, arguments); // Default label emphasis `show` defaultEmphasis(option, 'label', ['show']); }, optionUpdated: function () { var option = this.option; var self = this; option.regions = geoCreator.getFilledRegions(option.regions, option.map, option.nameMap); this._optionModelMap = reduce(option.regions || [], function (optionModelMap, regionOpt) { if (regionOpt.name) { optionModelMap.set(regionOpt.name, new Model(regionOpt, self)); } return optionModelMap; }, createHashMap()); this.updateSelectedMap(option.regions); }, defaultOption: { zlevel: 0, z: 0, show: true, left: 'center', top: 'center', // width:, // height:, // right // bottom // Aspect is width / height. Inited to be geoJson bbox aspect // This parameter is used for scale this aspect aspectScale: 0.75, ///// Layout with center and size // If you wan't to put map in a fixed size box with right aspect ratio // This two properties may more conveninet // layoutCenter: [50%, 50%] // layoutSize: 100 silent: false, // Map type map: '', // Define left-top, right-bottom coords to control view // For example, [ [180, 90], [-180, -90] ] boundingCoords: null, // Default on center of map center: null, zoom: 1, scaleLimit: null, // selectedMode: false label: { show: false, color: '#000' }, itemStyle: { // color: 各异, borderWidth: 0.5, borderColor: '#444', color: '#eee' }, emphasis: { label: { show: true, color: 'rgb(100,0,0)' }, itemStyle: { color: 'rgba(255,215,0,0.8)' } }, regions: [] }, /** * Get model of region * @param {string} name * @return {module:echarts/model/Model} */ getRegionModel: function (name) { return this._optionModelMap.get(name) || new Model(null, this, this.ecModel); }, /** * Format label * @param {string} name Region name * @param {string} [status='normal'] 'normal' or 'emphasis' * @return {string} */ getFormattedLabel: function (name, status) { var regionModel = this.getRegionModel(name); var formatter = regionModel.get('label.' + status + '.formatter'); var params = { name: name }; if (typeof formatter === 'function') { params.status = status; return formatter(params); } else if (typeof formatter === 'string') { return formatter.replace('{a}', name != null ? name : ''); } }, setZoom: function (zoom) { this.option.zoom = zoom; }, setCenter: function (center) { this.option.center = center; } }); mixin(GeoModel, selectableMixin); extendComponentView({ type: 'geo', init: function (ecModel, api) { var mapDraw = new MapDraw(api, true); this._mapDraw = mapDraw; this.group.add(mapDraw.group); }, render: function (geoModel, ecModel, api, payload) { // Not render if it is an toggleSelect action from self if (payload && payload.type === 'geoToggleSelect' && payload.from === this.uid ) { return; } var mapDraw = this._mapDraw; if (geoModel.get('show')) { mapDraw.draw(geoModel, ecModel, api, this, payload); } else { this._mapDraw.group.removeAll(); } this.group.silent = geoModel.get('silent'); }, dispose: function () { this._mapDraw && this._mapDraw.remove(); } }); function makeAction(method, actionInfo) { actionInfo.update = 'updateView'; registerAction(actionInfo, function (payload, ecModel) { var selected = {}; ecModel.eachComponent( { mainType: 'geo', query: payload}, function (geoModel) { geoModel[method](payload.name); var geo = geoModel.coordinateSystem; each$1(geo.regions, function (region) { selected[region.name] = geoModel.isSelected(region.name) || false; }); } ); return { selected: selected, name: payload.name }; }); } makeAction('toggleSelected', { type: 'geoToggleSelect', event: 'geoselectchanged' }); makeAction('select', { type: 'geoSelect', event: 'geoselected' }); makeAction('unSelect', { type: 'geoUnSelect', event: 'geounselected' }); var DEFAULT_TOOLBOX_BTNS = ['rect', 'polygon', 'keep', 'clear']; var preprocessor$1 = function (option, isNew) { var brushComponents = option && option.brush; if (!isArray(brushComponents)) { brushComponents = brushComponents ? [brushComponents] : []; } if (!brushComponents.length) { return; } var brushComponentSpecifiedBtns = []; each$1(brushComponents, function (brushOpt) { var tbs = brushOpt.hasOwnProperty('toolbox') ? brushOpt.toolbox : []; if (tbs instanceof Array) { brushComponentSpecifiedBtns = brushComponentSpecifiedBtns.concat(tbs); } }); var toolbox = option && option.toolbox; if (isArray(toolbox)) { toolbox = toolbox[0]; } if (!toolbox) { toolbox = {feature: {}}; option.toolbox = [toolbox]; } var toolboxFeature = (toolbox.feature || (toolbox.feature = {})); var toolboxBrush = toolboxFeature.brush || (toolboxFeature.brush = {}); var brushTypes = toolboxBrush.type || (toolboxBrush.type = []); brushTypes.push.apply(brushTypes, brushComponentSpecifiedBtns); removeDuplicate(brushTypes); if (isNew && !brushTypes.length) { brushTypes.push.apply(brushTypes, DEFAULT_TOOLBOX_BTNS); } }; function removeDuplicate(arr) { var map$$1 = {}; each$1(arr, function (val) { map$$1[val] = 1; }); arr.length = 0; each$1(map$$1, function (flag, val) { arr.push(val); }); } /** * @file Visual solution, for consistent option specification. */ var each$20 = each$1; function hasKeys(obj) { if (obj) { for (var name in obj){ if (obj.hasOwnProperty(name)) { return true; } } } } /** * @param {Object} option * @param {Array.<string>} stateList * @param {Function} [supplementVisualOption] * @return {Object} visualMappings <state, <visualType, module:echarts/visual/VisualMapping>> */ function createVisualMappings(option, stateList, supplementVisualOption) { var visualMappings = {}; each$20(stateList, function (state) { var mappings = visualMappings[state] = createMappings(); each$20(option[state], function (visualData, visualType) { if (!VisualMapping.isValidType(visualType)) { return; } var mappingOption = { type: visualType, visual: visualData }; supplementVisualOption && supplementVisualOption(mappingOption, state); mappings[visualType] = new VisualMapping(mappingOption); // Prepare a alpha for opacity, for some case that opacity // is not supported, such as rendering using gradient color. if (visualType === 'opacity') { mappingOption = clone(mappingOption); mappingOption.type = 'colorAlpha'; mappings.__hidden.__alphaForOpacity = new VisualMapping(mappingOption); } }); }); return visualMappings; function createMappings() { var Creater = function () {}; // Make sure hidden fields will not be visited by // object iteration (with hasOwnProperty checking). Creater.prototype.__hidden = Creater.prototype; var obj = new Creater(); return obj; } } /** * @param {Object} thisOption * @param {Object} newOption * @param {Array.<string>} keys */ function replaceVisualOption(thisOption, newOption, keys) { // Visual attributes merge is not supported, otherwise it // brings overcomplicated merge logic. See #2853. So if // newOption has anyone of these keys, all of these keys // will be reset. Otherwise, all keys remain. var has; each$1(keys, function (key) { if (newOption.hasOwnProperty(key) && hasKeys(newOption[key])) { has = true; } }); has && each$1(keys, function (key) { if (newOption.hasOwnProperty(key) && hasKeys(newOption[key])) { thisOption[key] = clone(newOption[key]); } else { delete thisOption[key]; } }); } /** * @param {Array.<string>} stateList * @param {Object} visualMappings <state, Object.<visualType, module:echarts/visual/VisualMapping>> * @param {module:echarts/data/List} list * @param {Function} getValueState param: valueOrIndex, return: state. * @param {object} [scope] Scope for getValueState * @param {string} [dimension] Concrete dimension, if used. */ // ???! handle brush? function applyVisual(stateList, visualMappings, data, getValueState, scope, dimension) { var visualTypesMap = {}; each$1(stateList, function (state) { var visualTypes = VisualMapping.prepareVisualTypes(visualMappings[state]); visualTypesMap[state] = visualTypes; }); var dataIndex; function getVisual(key) { return data.getItemVisual(dataIndex, key); } function setVisual(key, value) { data.setItemVisual(dataIndex, key, value); } if (dimension == null) { data.each(eachItem); } else { data.each([dimension], eachItem); } function eachItem(valueOrIndex, index) { dataIndex = dimension == null ? valueOrIndex : index; var rawDataItem = data.getRawDataItem(dataIndex); // Consider performance if (rawDataItem && rawDataItem.visualMap === false) { return; } var valueState = getValueState.call(scope, valueOrIndex); var mappings = visualMappings[valueState]; var visualTypes = visualTypesMap[valueState]; for (var i = 0, len = visualTypes.length; i < len; i++) { var type = visualTypes[i]; mappings[type] && mappings[type].applyVisual( valueOrIndex, getVisual, setVisual ); } } } /** * @param {module:echarts/data/List} data * @param {Array.<string>} stateList * @param {Object} visualMappings <state, Object.<visualType, module:echarts/visual/VisualMapping>> * @param {Function} getValueState param: valueOrIndex, return: state. * @param {number} [dim] dimension or dimension index. */ function incrementalApplyVisual(stateList, visualMappings, getValueState, dim) { var visualTypesMap = {}; each$1(stateList, function (state) { var visualTypes = VisualMapping.prepareVisualTypes(visualMappings[state]); visualTypesMap[state] = visualTypes; }); function progress(params, data) { if (dim != null) { dim = data.getDimension(dim); } function getVisual(key) { return data.getItemVisual(dataIndex, key); } function setVisual(key, value) { data.setItemVisual(dataIndex, key, value); } for (var dataIndex = params.start; dataIndex < params.end; dataIndex++) { var rawDataItem = data.getRawDataItem(dataIndex); // Consider performance if (rawDataItem && rawDataItem.visualMap === false) { return; } var value = dim != null ? data.get(dim, dataIndex, true) : dataIndex; var valueState = getValueState(value); var mappings = visualMappings[valueState]; var visualTypes = visualTypesMap[valueState]; for (var i = 0, len = visualTypes.length; i < len; i++) { var type = visualTypes[i]; mappings[type] && mappings[type].applyVisual(value, getVisual, setVisual); } } } return {progress: progress}; } // Key of the first level is brushType: `line`, `rect`, `polygon`. // Key of the second level is chart element type: `point`, `rect`. // See moudule:echarts/component/helper/BrushController // function param: // {Object} itemLayout fetch from data.getItemLayout(dataIndex) // {Object} selectors {point: selector, rect: selector, ...} // {Object} area {range: [[], [], ..], boudingRect} // function return: // {boolean} Whether in the given brush. var selector = { lineX: getLineSelectors(0), lineY: getLineSelectors(1), rect: { point: function (itemLayout, selectors, area) { return itemLayout && area.boundingRect.contain(itemLayout[0], itemLayout[1]); }, rect: function (itemLayout, selectors, area) { return itemLayout && area.boundingRect.intersect(itemLayout); } }, polygon: { point: function (itemLayout, selectors, area) { return itemLayout && area.boundingRect.contain(itemLayout[0], itemLayout[1]) && contain$1(area.range, itemLayout[0], itemLayout[1]); }, rect: function (itemLayout, selectors, area) { var points = area.range; if (!itemLayout || points.length <= 1) { return false; } var x = itemLayout.x; var y = itemLayout.y; var width = itemLayout.width; var height = itemLayout.height; var p = points[0]; if (contain$1(points, x, y) || contain$1(points, x + width, y) || contain$1(points, x, y + height) || contain$1(points, x + width, y + height) || BoundingRect.create(itemLayout).contain(p[0], p[1]) || lineIntersectPolygon(x, y, x + width, y, points) || lineIntersectPolygon(x, y, x, y + height, points) || lineIntersectPolygon(x + width, y, x + width, y + height, points) || lineIntersectPolygon(x, y + height, x + width, y + height, points) ) { return true; } } } }; function getLineSelectors(xyIndex) { var xy = ['x', 'y']; var wh = ['width', 'height']; return { point: function (itemLayout, selectors, area) { if (itemLayout) { var range = area.range; var p = itemLayout[xyIndex]; return inLineRange(p, range); } }, rect: function (itemLayout, selectors, area) { if (itemLayout) { var range = area.range; var layoutRange = [ itemLayout[xy[xyIndex]], itemLayout[xy[xyIndex]] + itemLayout[wh[xyIndex]] ]; layoutRange[1] < layoutRange[0] && layoutRange.reverse(); return inLineRange(layoutRange[0], range) || inLineRange(layoutRange[1], range) || inLineRange(range[0], layoutRange) || inLineRange(range[1], layoutRange); } } }; } function inLineRange(p, range) { return range[0] <= p && p <= range[1]; } function lineIntersectPolygon(lx, ly, l2x, l2y, points) { for (var i = 0, p2 = points[points.length - 1]; i < points.length; i++) { var p = points[i]; if (lineIntersect(lx, ly, l2x, l2y, p[0], p[1], p2[0], p2[1])) { return true; } p2 = p; } } // Code from <http://blog.csdn.net/rickliuxiao/article/details/6259322> with some fix. // See <https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection> function lineIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) { var delta = determinant(a2x - a1x, b1x - b2x, a2y - a1y, b1y - b2y); if (nearZero(delta)) { // parallel return false; } var namenda = determinant(b1x - a1x, b1x - b2x, b1y - a1y, b1y - b2y) / delta; if (namenda < 0 || namenda > 1) { return false; } var miu = determinant(a2x - a1x, b1x - a1x, a2y - a1y, b1y - a1y) / delta; if (miu < 0 || miu > 1) { return false; } return true; } function nearZero(val) { return val <= (1e-6) && val >= -(1e-6); } function determinant(v1, v2, v3, v4) { return v1 * v4 - v2 * v3; } var each$21 = each$1; var indexOf$1 = indexOf; var curry$5 = curry; var COORD_CONVERTS = ['dataToPoint', 'pointToData']; // FIXME // how to genarialize to more coordinate systems. var INCLUDE_FINDER_MAIN_TYPES = [ 'grid', 'xAxis', 'yAxis', 'geo', 'graph', 'polar', 'radiusAxis', 'angleAxis', 'bmap' ]; /** * [option in constructor]: * { * Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder. * } * * * [targetInfo]: * * There can be multiple axes in a single targetInfo. Consider the case * of `grid` component, a targetInfo represents a grid which contains one or more * cartesian and one or more axes. And consider the case of parallel system, * which has multiple axes in a coordinate system. * Can be { * panelId: ..., * coordSys: <a representitive cartesian in grid (first cartesian by default)>, * coordSyses: all cartesians. * gridModel: <grid component> * xAxes: correspond to coordSyses on index * yAxes: correspond to coordSyses on index * } * or { * panelId: ..., * coordSys: <geo coord sys> * coordSyses: [<geo coord sys>] * geoModel: <geo component> * } * * * [panelOpt]: * * Make from targetInfo. Input to BrushController. * { * panelId: ..., * rect: ... * } * * * [area]: * * Generated by BrushController or user input. * { * panelId: Used to locate coordInfo directly. If user inpput, no panelId. * brushType: determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y'). * Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder. * range: pixel range. * coordRange: representitive coord range (the first one of coordRanges). * coordRanges: <Array> coord ranges, used in multiple cartesian in one grid. * } */ /** * @param {Object} option contains Index/Id/Name of xAxis/yAxis/geo/grid * Each can be {number|Array.<number>}. like: {xAxisIndex: [3, 4]} * @param {module:echarts/model/Global} ecModel * @param {Object} [opt] * @param {Array.<string>} [opt.include] include coordinate system types. */ function BrushTargetManager(option, ecModel, opt) { /** * @private * @type {Array.<Object>} */ var targetInfoList = this._targetInfoList = []; var info = {}; var foundCpts = parseFinder$1(ecModel, option); each$21(targetInfoBuilders, function (builder, type) { if (!opt || !opt.include || indexOf$1(opt.include, type) >= 0) { builder(foundCpts, targetInfoList, info); } }); } var proto$2 = BrushTargetManager.prototype; proto$2.setOutputRanges = function (areas, ecModel) { this.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys) { (area.coordRanges || (area.coordRanges = [])).push(coordRange); // area.coordRange is the first of area.coordRanges if (!area.coordRange) { area.coordRange = coordRange; // In 'category' axis, coord to pixel is not reversible, so we can not // rebuild range by coordRange accrately, which may bring trouble when // brushing only one item. So we use __rangeOffset to rebuilding range // by coordRange. And this it only used in brush component so it is no // need to be adapted to coordRanges. var result = coordConvert[area.brushType](0, coordSys, coordRange); area.__rangeOffset = { offset: diffProcessor[area.brushType](result.values, area.range, [1, 1]), xyMinMax: result.xyMinMax }; } }); }; proto$2.matchOutputRanges = function (areas, ecModel, cb) { each$21(areas, function (area) { var targetInfo = this.findTargetInfo(area, ecModel); if (targetInfo && targetInfo !== true) { each$1( targetInfo.coordSyses, function (coordSys) { var result = coordConvert[area.brushType](1, coordSys, area.range); cb(area, result.values, coordSys, ecModel); } ); } }, this); }; proto$2.setInputRanges = function (areas, ecModel) { each$21(areas, function (area) { var targetInfo = this.findTargetInfo(area, ecModel); if (__DEV__) { assert$1( !targetInfo || targetInfo === true || area.coordRange, 'coordRange must be specified when coord index specified.' ); assert$1( !targetInfo || targetInfo !== true || area.range, 'range must be specified in global brush.' ); } area.range = area.range || []; // convert coordRange to global range and set panelId. if (targetInfo && targetInfo !== true) { area.panelId = targetInfo.panelId; // (1) area.range shoule always be calculate from coordRange but does // not keep its original value, for the sake of the dataZoom scenario, // where area.coordRange remains unchanged but area.range may be changed. // (2) Only support converting one coordRange to pixel range in brush // component. So do not consider `coordRanges`. // (3) About __rangeOffset, see comment above. var result = coordConvert[area.brushType](0, targetInfo.coordSys, area.coordRange); var rangeOffset = area.__rangeOffset; area.range = rangeOffset ? diffProcessor[area.brushType]( result.values, rangeOffset.offset, getScales(result.xyMinMax, rangeOffset.xyMinMax) ) : result.values; } }, this); }; proto$2.makePanelOpts = function (api, getDefaultBrushType) { return map(this._targetInfoList, function (targetInfo) { var rect = targetInfo.getPanelRect(); return { panelId: targetInfo.panelId, defaultBrushType: getDefaultBrushType && getDefaultBrushType(targetInfo), clipPath: makeRectPanelClipPath(rect), isTargetByCursor: makeRectIsTargetByCursor( rect, api, targetInfo.coordSysModel ), getLinearBrushOtherExtent: makeLinearBrushOtherExtent(rect) }; }); }; proto$2.controlSeries = function (area, seriesModel, ecModel) { // Check whether area is bound in coord, and series do not belong to that coord. // If do not do this check, some brush (like lineX) will controll all axes. var targetInfo = this.findTargetInfo(area, ecModel); return targetInfo === true || ( targetInfo && indexOf$1(targetInfo.coordSyses, seriesModel.coordinateSystem) >= 0 ); }; /** * If return Object, a coord found. * If reutrn true, global found. * Otherwise nothing found. * * @param {Object} area * @param {Array} targetInfoList * @return {Object|boolean} */ proto$2.findTargetInfo = function (area, ecModel) { var targetInfoList = this._targetInfoList; var foundCpts = parseFinder$1(ecModel, area); for (var i = 0; i < targetInfoList.length; i++) { var targetInfo = targetInfoList[i]; var areaPanelId = area.panelId; if (areaPanelId) { if (targetInfo.panelId === areaPanelId) { return targetInfo; } } else { for (var i = 0; i < targetInfoMatchers.length; i++) { if (targetInfoMatchers[i](foundCpts, targetInfo)) { return targetInfo; } } } } return true; }; function formatMinMax(minMax) { minMax[0] > minMax[1] && minMax.reverse(); return minMax; } function parseFinder$1(ecModel, option) { return parseFinder( ecModel, option, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES} ); } var targetInfoBuilders = { grid: function (foundCpts, targetInfoList) { var xAxisModels = foundCpts.xAxisModels; var yAxisModels = foundCpts.yAxisModels; var gridModels = foundCpts.gridModels; // Remove duplicated. var gridModelMap = createHashMap(); var xAxesHas = {}; var yAxesHas = {}; if (!xAxisModels && !yAxisModels && !gridModels) { return; } each$21(xAxisModels, function (axisModel) { var gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; }); each$21(yAxisModels, function (axisModel) { var gridModel = axisModel.axis.grid.model; gridModelMap.set(gridModel.id, gridModel); yAxesHas[gridModel.id] = true; }); each$21(gridModels, function (gridModel) { gridModelMap.set(gridModel.id, gridModel); xAxesHas[gridModel.id] = true; yAxesHas[gridModel.id] = true; }); gridModelMap.each(function (gridModel) { var grid = gridModel.coordinateSystem; var cartesians = []; each$21(grid.getCartesians(), function (cartesian, index) { if (indexOf$1(xAxisModels, cartesian.getAxis('x').model) >= 0 || indexOf$1(yAxisModels, cartesian.getAxis('y').model) >= 0 ) { cartesians.push(cartesian); } }); targetInfoList.push({ panelId: 'grid--' + gridModel.id, gridModel: gridModel, coordSysModel: gridModel, // Use the first one as the representitive coordSys. coordSys: cartesians[0], coordSyses: cartesians, getPanelRect: panelRectBuilder.grid, xAxisDeclared: xAxesHas[gridModel.id], yAxisDeclared: yAxesHas[gridModel.id] }); }); }, geo: function (foundCpts, targetInfoList) { each$21(foundCpts.geoModels, function (geoModel) { var coordSys = geoModel.coordinateSystem; targetInfoList.push({ panelId: 'geo--' + geoModel.id, geoModel: geoModel, coordSysModel: geoModel, coordSys: coordSys, coordSyses: [coordSys], getPanelRect: panelRectBuilder.geo }); }); } }; var targetInfoMatchers = [ // grid function (foundCpts, targetInfo) { var xAxisModel = foundCpts.xAxisModel; var yAxisModel = foundCpts.yAxisModel; var gridModel = foundCpts.gridModel; !gridModel && xAxisModel && (gridModel = xAxisModel.axis.grid.model); !gridModel && yAxisModel && (gridModel = yAxisModel.axis.grid.model); return gridModel && gridModel === targetInfo.gridModel; }, // geo function (foundCpts, targetInfo) { var geoModel = foundCpts.geoModel; return geoModel && geoModel === targetInfo.geoModel; } ]; var panelRectBuilder = { grid: function () { // grid is not Transformable. return this.coordSys.grid.getRect().clone(); }, geo: function () { var coordSys = this.coordSys; var rect = coordSys.getBoundingRect().clone(); // geo roam and zoom transform rect.applyTransform(getTransform(coordSys)); return rect; } }; var coordConvert = { lineX: curry$5(axisConvert, 0), lineY: curry$5(axisConvert, 1), rect: function (to, coordSys, rangeOrCoordRange) { var xminymin = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]]); var xmaxymax = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]]); var values = [ formatMinMax([xminymin[0], xmaxymax[0]]), formatMinMax([xminymin[1], xmaxymax[1]]) ]; return {values: values, xyMinMax: values}; }, polygon: function (to, coordSys, rangeOrCoordRange) { var xyMinMax = [[Infinity, -Infinity], [Infinity, -Infinity]]; var values = map(rangeOrCoordRange, function (item) { var p = coordSys[COORD_CONVERTS[to]](item); xyMinMax[0][0] = Math.min(xyMinMax[0][0], p[0]); xyMinMax[1][0] = Math.min(xyMinMax[1][0], p[1]); xyMinMax[0][1] = Math.max(xyMinMax[0][1], p[0]); xyMinMax[1][1] = Math.max(xyMinMax[1][1], p[1]); return p; }); return {values: values, xyMinMax: xyMinMax}; } }; function axisConvert(axisNameIndex, to, coordSys, rangeOrCoordRange) { if (__DEV__) { assert$1( coordSys.type === 'cartesian2d', 'lineX/lineY brush is available only in cartesian2d.' ); } var axis = coordSys.getAxis(['x', 'y'][axisNameIndex]); var values = formatMinMax(map([0, 1], function (i) { return to ? axis.coordToData(axis.toLocalCoord(rangeOrCoordRange[i])) : axis.toGlobalCoord(axis.dataToCoord(rangeOrCoordRange[i])); })); var xyMinMax = []; xyMinMax[axisNameIndex] = values; xyMinMax[1 - axisNameIndex] = [NaN, NaN]; return {values: values, xyMinMax: xyMinMax}; } var diffProcessor = { lineX: curry$5(axisDiffProcessor, 0), lineY: curry$5(axisDiffProcessor, 1), rect: function (values, refer, scales) { return [ [values[0][0] - scales[0] * refer[0][0], values[0][1] - scales[0] * refer[0][1]], [values[1][0] - scales[1] * refer[1][0], values[1][1] - scales[1] * refer[1][1]] ]; }, polygon: function (values, refer, scales) { return map(values, function (item, idx) { return [item[0] - scales[0] * refer[idx][0], item[1] - scales[1] * refer[idx][1]]; }); } }; function axisDiffProcessor(axisNameIndex, values, refer, scales) { return [ values[0] - scales[axisNameIndex] * refer[0], values[1] - scales[axisNameIndex] * refer[1] ]; } // We have to process scale caused by dataZoom manually, // although it might be not accurate. function getScales(xyMinMaxCurr, xyMinMaxOrigin) { var sizeCurr = getSize(xyMinMaxCurr); var sizeOrigin = getSize(xyMinMaxOrigin); var scales = [sizeCurr[0] / sizeOrigin[0], sizeCurr[1] / sizeOrigin[1]]; isNaN(scales[0]) && (scales[0] = 1); isNaN(scales[1]) && (scales[1] = 1); return scales; } function getSize(xyMinMax) { return xyMinMax ? [xyMinMax[0][1] - xyMinMax[0][0], xyMinMax[1][1] - xyMinMax[1][0]] : [NaN, NaN]; } var STATE_LIST = ['inBrush', 'outOfBrush']; var DISPATCH_METHOD = '__ecBrushSelect'; var DISPATCH_FLAG = '__ecInBrushSelectEvent'; var PRIORITY_BRUSH = PRIORITY.VISUAL.BRUSH; /** * Layout for visual, the priority higher than other layout, and before brush visual. */ registerLayout(PRIORITY_BRUSH, function (ecModel, api, payload) { ecModel.eachComponent({mainType: 'brush'}, function (brushModel) { payload && payload.type === 'takeGlobalCursor' && brushModel.setBrushOption( payload.key === 'brush' ? payload.brushOption : {brushType: false} ); var brushTargetManager = brushModel.brushTargetManager = new BrushTargetManager(brushModel.option, ecModel); brushTargetManager.setInputRanges(brushModel.areas, ecModel); }); }); /** * Register the visual encoding if this modules required. */ registerVisual(PRIORITY_BRUSH, function (ecModel, api, payload) { var brushSelected = []; var throttleType; var throttleDelay; ecModel.eachComponent({mainType: 'brush'}, function (brushModel, brushIndex) { var thisBrushSelected = { brushId: brushModel.id, brushIndex: brushIndex, brushName: brushModel.name, areas: clone(brushModel.areas), selected: [] }; // Every brush component exists in event params, convenient // for user to find by index. brushSelected.push(thisBrushSelected); var brushOption = brushModel.option; var brushLink = brushOption.brushLink; var linkedSeriesMap = []; var selectedDataIndexForLink = []; var rangeInfoBySeries = []; var hasBrushExists = 0; if (!brushIndex) { // Only the first throttle setting works. throttleType = brushOption.throttleType; throttleDelay = brushOption.throttleDelay; } // Add boundingRect and selectors to range. var areas = map(brushModel.areas, function (area) { return bindSelector( defaults( {boundingRect: boundingRectBuilders[area.brushType](area)}, area ) ); }); var visualMappings = createVisualMappings( brushModel.option, STATE_LIST, function (mappingOption) { mappingOption.mappingMethod = 'fixed'; } ); isArray(brushLink) && each$1(brushLink, function (seriesIndex) { linkedSeriesMap[seriesIndex] = 1; }); function linkOthers(seriesIndex) { return brushLink === 'all' || linkedSeriesMap[seriesIndex]; } // If no supported brush or no brush on the series, // all visuals should be in original state. function brushed(rangeInfoList) { return !!rangeInfoList.length; } /** * Logic for each series: (If the logic has to be modified one day, do it carefully!) * * ( brushed ┬ && ┬hasBrushExist ┬ && linkOthers ) => StepA: ┬record, ┬ StepB: ┬visualByRecord. * !brushed┘ ├hasBrushExist ┤ └nothing,┘ ├visualByRecord. * └!hasBrushExist┘ └nothing. * ( !brushed && ┬hasBrushExist ┬ && linkOthers ) => StepA: nothing, StepB: ┬visualByRecord. * └!hasBrushExist┘ └nothing. * ( brushed ┬ && !linkOthers ) => StepA: nothing, StepB: ┬visualByCheck. * !brushed┘ └nothing. * ( !brushed && !linkOthers ) => StepA: nothing, StepB: nothing. */ // Step A ecModel.eachSeries(function (seriesModel, seriesIndex) { var rangeInfoList = rangeInfoBySeries[seriesIndex] = []; seriesModel.subType === 'parallel' ? stepAParallel(seriesModel, seriesIndex, rangeInfoList) : stepAOthers(seriesModel, seriesIndex, rangeInfoList); }); function stepAParallel(seriesModel, seriesIndex) { var coordSys = seriesModel.coordinateSystem; hasBrushExists |= coordSys.hasAxisBrushed(); linkOthers(seriesIndex) && coordSys.eachActiveState( seriesModel.getData(), function (activeState, dataIndex) { activeState === 'active' && (selectedDataIndexForLink[dataIndex] = 1); } ); } function stepAOthers(seriesModel, seriesIndex, rangeInfoList) { var selectorsByBrushType = getSelectorsByBrushType(seriesModel); if (!selectorsByBrushType || brushModelNotControll(brushModel, seriesIndex)) { return; } each$1(areas, function (area) { selectorsByBrushType[area.brushType] && brushModel.brushTargetManager.controlSeries(area, seriesModel, ecModel) && rangeInfoList.push(area); hasBrushExists |= brushed(rangeInfoList); }); if (linkOthers(seriesIndex) && brushed(rangeInfoList)) { var data = seriesModel.getData(); data.each(function (dataIndex) { if (checkInRange(selectorsByBrushType, rangeInfoList, data, dataIndex)) { selectedDataIndexForLink[dataIndex] = 1; } }); } } // Step B ecModel.eachSeries(function (seriesModel, seriesIndex) { var seriesBrushSelected = { seriesId: seriesModel.id, seriesIndex: seriesIndex, seriesName: seriesModel.name, dataIndex: [] }; // Every series exists in event params, convenient // for user to find series by seriesIndex. thisBrushSelected.selected.push(seriesBrushSelected); var selectorsByBrushType = getSelectorsByBrushType(seriesModel); var rangeInfoList = rangeInfoBySeries[seriesIndex]; var data = seriesModel.getData(); var getValueState = linkOthers(seriesIndex) ? function (dataIndex) { return selectedDataIndexForLink[dataIndex] ? (seriesBrushSelected.dataIndex.push(data.getRawIndex(dataIndex)), 'inBrush') : 'outOfBrush'; } : function (dataIndex) { return checkInRange(selectorsByBrushType, rangeInfoList, data, dataIndex) ? (seriesBrushSelected.dataIndex.push(data.getRawIndex(dataIndex)), 'inBrush') : 'outOfBrush'; }; // If no supported brush or no brush, all visuals are in original state. (linkOthers(seriesIndex) ? hasBrushExists : brushed(rangeInfoList)) && applyVisual( STATE_LIST, visualMappings, data, getValueState ); }); }); dispatchAction(api, throttleType, throttleDelay, brushSelected, payload); }); function dispatchAction(api, throttleType, throttleDelay, brushSelected, payload) { // This event will not be triggered when `setOpion`, otherwise dead lock may // triggered when do `setOption` in event listener, which we do not find // satisfactory way to solve yet. Some considered resolutions: // (a) Diff with prevoius selected data ant only trigger event when changed. // But store previous data and diff precisely (i.e., not only by dataIndex, but // also detect value changes in selected data) might bring complexity or fragility. // (b) Use spectial param like `silent` to suppress event triggering. // But such kind of volatile param may be weird in `setOption`. if (!payload) { return; } var zr = api.getZr(); if (zr[DISPATCH_FLAG]) { return; } if (!zr[DISPATCH_METHOD]) { zr[DISPATCH_METHOD] = doDispatch; } var fn = createOrUpdate(zr, DISPATCH_METHOD, throttleDelay, throttleType); fn(api, brushSelected); } function doDispatch(api, brushSelected) { if (!api.isDisposed()) { var zr = api.getZr(); zr[DISPATCH_FLAG] = true; api.dispatchAction({ type: 'brushSelect', batch: brushSelected }); zr[DISPATCH_FLAG] = false; } } function checkInRange(selectorsByBrushType, rangeInfoList, data, dataIndex) { for (var i = 0, len = rangeInfoList.length; i < len; i++) { var area = rangeInfoList[i]; if (selectorsByBrushType[area.brushType]( dataIndex, data, area.selectors, area )) { return true; } } } function getSelectorsByBrushType(seriesModel) { var brushSelector = seriesModel.brushSelector; if (isString(brushSelector)) { var sels = []; each$1(selector, function (selectorsByElementType, brushType) { sels[brushType] = function (dataIndex, data, selectors, area) { var itemLayout = data.getItemLayout(dataIndex); return selectorsByElementType[brushSelector](itemLayout, selectors, area); }; }); return sels; } else if (isFunction$1(brushSelector)) { var bSelector = {}; each$1(selector, function (sel, brushType) { bSelector[brushType] = brushSelector; }); return bSelector; } return brushSelector; } function brushModelNotControll(brushModel, seriesIndex) { var seriesIndices = brushModel.option.seriesIndex; return seriesIndices != null && seriesIndices !== 'all' && ( isArray(seriesIndices) ? indexOf(seriesIndices, seriesIndex) < 0 : seriesIndex !== seriesIndices ); } function bindSelector(area) { var selectors = area.selectors = {}; each$1(selector[area.brushType], function (selFn, elType) { // Do not use function binding or curry for performance. selectors[elType] = function (itemLayout) { return selFn(itemLayout, selectors, area); }; }); return area; } var boundingRectBuilders = { lineX: noop, lineY: noop, rect: function (area) { return getBoundingRectFromMinMax(area.range); }, polygon: function (area) { var minMax; var range = area.range; for (var i = 0, len = range.length; i < len; i++) { minMax = minMax || [[Infinity, -Infinity], [Infinity, -Infinity]]; var rg = range[i]; rg[0] < minMax[0][0] && (minMax[0][0] = rg[0]); rg[0] > minMax[0][1] && (minMax[0][1] = rg[0]); rg[1] < minMax[1][0] && (minMax[1][0] = rg[1]); rg[1] > minMax[1][1] && (minMax[1][1] = rg[1]); } return minMax && getBoundingRectFromMinMax(minMax); } }; function getBoundingRectFromMinMax(minMax) { return new BoundingRect( minMax[0][0], minMax[1][0], minMax[0][1] - minMax[0][0], minMax[1][1] - minMax[1][0] ); } var DEFAULT_OUT_OF_BRUSH_COLOR = ['#ddd']; var BrushModel = extendComponentModel({ type: 'brush', dependencies: ['geo', 'grid', 'xAxis', 'yAxis', 'parallel', 'series'], /** * @protected */ defaultOption: { // inBrush: null, // outOfBrush: null, toolbox: null, // Default value see preprocessor. brushLink: null, // Series indices array, broadcast using dataIndex. // or 'all', which means all series. 'none' or null means no series. seriesIndex: 'all', // seriesIndex array, specify series controlled by this brush component. geoIndex: null, // xAxisIndex: null, yAxisIndex: null, brushType: 'rect', // Default brushType, see BrushController. brushMode: 'single', // Default brushMode, 'single' or 'multiple' transformable: true, // Default transformable. brushStyle: { // Default brushStyle borderWidth: 1, color: 'rgba(120,140,180,0.3)', borderColor: 'rgba(120,140,180,0.8)' }, throttleType: 'fixRate',// Throttle in brushSelected event. 'fixRate' or 'debounce'. // If null, no throttle. Valid only in the first brush component throttleDelay: 0, // Unit: ms, 0 means every event will be triggered. // FIXME // 试验效果 removeOnClick: true, z: 10000 }, /** * @readOnly * @type {Array.<Object>} */ areas: [], /** * Current activated brush type. * If null, brush is inactived. * see module:echarts/component/helper/BrushController * @readOnly * @type {string} */ brushType: null, /** * Current brush opt. * see module:echarts/component/helper/BrushController * @readOnly * @type {Object} */ brushOption: {}, /** * @readOnly * @type {Array.<Object>} */ coordInfoList: [], optionUpdated: function (newOption, isInit) { var thisOption = this.option; !isInit && replaceVisualOption( thisOption, newOption, ['inBrush', 'outOfBrush'] ); thisOption.inBrush = thisOption.inBrush || {}; // Always give default visual, consider setOption at the second time. thisOption.outOfBrush = thisOption.outOfBrush || {color: DEFAULT_OUT_OF_BRUSH_COLOR}; }, /** * If ranges is null/undefined, range state remain. * * @param {Array.<Object>} [ranges] */ setAreas: function (areas) { if (__DEV__) { assert$1(isArray(areas)); each$1(areas, function (area) { assert$1(area.brushType, 'Illegal areas'); }); } // If ranges is null/undefined, range state remain. // This helps user to dispatchAction({type: 'brush'}) with no areas // set but just want to get the current brush select info from a `brush` event. if (!areas) { return; } this.areas = map(areas, function (area) { return generateBrushOption(this.option, area); }, this); }, /** * see module:echarts/component/helper/BrushController * @param {Object} brushOption */ setBrushOption: function (brushOption) { this.brushOption = generateBrushOption(this.option, brushOption); this.brushType = this.brushOption.brushType; } }); function generateBrushOption(option, brushOption) { return merge( { brushType: option.brushType, brushMode: option.brushMode, transformable: option.transformable, brushStyle: new Model(option.brushStyle).getItemStyle(), removeOnClick: option.removeOnClick, z: option.z }, brushOption, true ); } extendComponentView({ type: 'brush', init: function (ecModel, api) { /** * @readOnly * @type {module:echarts/model/Global} */ this.ecModel = ecModel; /** * @readOnly * @type {module:echarts/ExtensionAPI} */ this.api = api; /** * @readOnly * @type {module:echarts/component/brush/BrushModel} */ this.model; /** * @private * @type {module:echarts/component/helper/BrushController} */ (this._brushController = new BrushController(api.getZr())) .on('brush', bind(this._onBrush, this)) .mount(); }, /** * @override */ render: function (brushModel) { this.model = brushModel; return updateController.apply(this, arguments); }, /** * @override */ updateTransform: updateController, /** * @override */ updateView: updateController, // /** // * @override // */ // updateLayout: updateController, // /** // * @override // */ // updateVisual: updateController, /** * @override */ dispose: function () { this._brushController.dispose(); }, /** * @private */ _onBrush: function (areas, opt) { var modelId = this.model.id; this.model.brushTargetManager.setOutputRanges(areas, this.ecModel); // Action is not dispatched on drag end, because the drag end // emits the same params with the last drag move event, and // may have some delay when using touch pad, which makes // animation not smooth (when using debounce). (!opt.isEnd || opt.removeOnClick) && this.api.dispatchAction({ type: 'brush', brushId: modelId, areas: clone(areas), $from: modelId }); } }); function updateController(brushModel, ecModel, api, payload) { // Do not update controller when drawing. (!payload || payload.$from !== brushModel.id) && this._brushController .setPanels(brushModel.brushTargetManager.makePanelOpts(api)) .enableBrush(brushModel.brushOption) .updateCovers(brushModel.areas.slice()); } /** * payload: { * brushIndex: number, or, * brushId: string, or, * brushName: string, * globalRanges: Array * } */ registerAction( {type: 'brush', event: 'brush' /*, update: 'updateView' */}, function (payload, ecModel) { ecModel.eachComponent({mainType: 'brush', query: payload}, function (brushModel) { brushModel.setAreas(payload.areas); }); } ); /** * payload: { * brushComponents: [ * { * brushId, * brushIndex, * brushName, * series: [ * { * seriesId, * seriesIndex, * seriesName, * rawIndices: [21, 34, ...] * }, * ... * ] * }, * ... * ] * } */ registerAction( {type: 'brushSelect', event: 'brushSelected', update: 'none'}, function () {} ); var features = {}; function register$1(name, ctor) { features[name] = ctor; } function get$1(name) { return features[name]; } var brushLang = lang.toolbox.brush; function Brush(model, ecModel, api) { this.model = model; this.ecModel = ecModel; this.api = api; /** * @private * @type {string} */ this._brushType; /** * @private * @type {string} */ this._brushMode; } Brush.defaultOption = { show: true, type: ['rect', 'polygon', 'lineX', 'lineY', 'keep', 'clear'], icon: { rect: 'M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13', // jshint ignore:line polygon: 'M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2', // jshint ignore:line lineX: 'M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4', // jshint ignore:line lineY: 'M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4', // jshint ignore:line keep: 'M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z', // jshint ignore:line clear: 'M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2' // jshint ignore:line }, // `rect`, `polygon`, `lineX`, `lineY`, `keep`, `clear` title: clone(brushLang.title) }; var proto$3 = Brush.prototype; // proto.updateLayout = function (featureModel, ecModel, api) { proto$3.render = proto$3.updateView = function (featureModel, ecModel, api) { var brushType; var brushMode; var isBrushed; ecModel.eachComponent({mainType: 'brush'}, function (brushModel) { brushType = brushModel.brushType; brushMode = brushModel.brushOption.brushMode || 'single'; isBrushed |= brushModel.areas.length; }); this._brushType = brushType; this._brushMode = brushMode; each$1(featureModel.get('type', true), function (type) { featureModel.setIconStatus( type, ( type === 'keep' ? brushMode === 'multiple' : type === 'clear' ? isBrushed : type === brushType ) ? 'emphasis' : 'normal' ); }); }; proto$3.getIcons = function () { var model = this.model; var availableIcons = model.get('icon', true); var icons = {}; each$1(model.get('type', true), function (type) { if (availableIcons[type]) { icons[type] = availableIcons[type]; } }); return icons; }; proto$3.onclick = function (ecModel, api, type) { var brushType = this._brushType; var brushMode = this._brushMode; if (type === 'clear') { // Trigger parallel action firstly api.dispatchAction({ type: 'axisAreaSelect', intervals: [] }); api.dispatchAction({ type: 'brush', command: 'clear', // Clear all areas of all brush components. areas: [] }); } else { api.dispatchAction({ type: 'takeGlobalCursor', key: 'brush', brushOption: { brushType: type === 'keep' ? brushType : (brushType === type ? false : type), brushMode: type === 'keep' ? (brushMode === 'multiple' ? 'single' : 'multiple') : brushMode } }); } }; register$1('brush', Brush); /** * Brush component entry */ registerPreprocessor(preprocessor$1); // (24*60*60*1000) var PROXIMATE_ONE_DAY = 86400000; /** * Calendar * * @constructor * * @param {Object} calendarModel calendarModel * @param {Object} ecModel ecModel * @param {Object} api api */ function Calendar(calendarModel, ecModel, api) { this._model = calendarModel; } Calendar.prototype = { constructor: Calendar, type: 'calendar', dimensions: ['time', 'value'], // Required in createListFromData getDimensionsInfo: function () { return [{name: 'time', type: 'time'}, 'value']; }, getRangeInfo: function () { return this._rangeInfo; }, getModel: function () { return this._model; }, getRect: function () { return this._rect; }, getCellWidth: function () { return this._sw; }, getCellHeight: function () { return this._sh; }, getOrient: function () { return this._orient; }, /** * getFirstDayOfWeek * * @example * 0 : start at Sunday * 1 : start at Monday * * @return {number} */ getFirstDayOfWeek: function () { return this._firstDayOfWeek; }, /** * get date info * * @param {string|number} date date * @return {Object} * { * y: string, local full year, eg., '1940', * m: string, local month, from '01' ot '12', * d: string, local date, from '01' to '31' (if exists), * day: It is not date.getDay(). It is the location of the cell in a week, from 0 to 6, * time: timestamp, * formatedDate: string, yyyy-MM-dd, * date: original date object. * } */ getDateInfo: function (date) { date = parseDate(date); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? '0' + m : m; var d = date.getDate(); d = d < 10 ? '0' + d : d; var day = date.getDay(); day = Math.abs((day + 7 - this.getFirstDayOfWeek()) % 7); return { y: y, m: m, d: d, day: day, time: date.getTime(), formatedDate: y + '-' + m + '-' + d, date: date }; }, getNextNDay: function (date, n) { n = n || 0; if (n === 0) { return this.getDateInfo(date); } date = new Date(this.getDateInfo(date).time); date.setDate(date.getDate() + n); return this.getDateInfo(date); }, update: function (ecModel, api) { this._firstDayOfWeek = +this._model.getModel('dayLabel').get('firstDay'); this._orient = this._model.get('orient'); this._lineWidth = this._model.getModel('itemStyle').getItemStyle().lineWidth || 0; this._rangeInfo = this._getRangeInfo(this._initRangeOption()); var weeks = this._rangeInfo.weeks || 1; var whNames = ['width', 'height']; var cellSize = this._model.get('cellSize').slice(); var layoutParams = this._model.getBoxLayoutParams(); var cellNumbers = this._orient === 'horizontal' ? [weeks, 7] : [7, weeks]; each$1([0, 1], function (idx) { if (cellSizeSpecified(cellSize, idx)) { layoutParams[whNames[idx]] = cellSize[idx] * cellNumbers[idx]; } }); var whGlobal = { width: api.getWidth(), height: api.getHeight() }; var calendarRect = this._rect = getLayoutRect(layoutParams, whGlobal); each$1([0, 1], function (idx) { if (!cellSizeSpecified(cellSize, idx)) { cellSize[idx] = calendarRect[whNames[idx]] / cellNumbers[idx]; } }); function cellSizeSpecified(cellSize, idx) { return cellSize[idx] != null && cellSize[idx] !== 'auto'; } this._sw = cellSize[0]; this._sh = cellSize[1]; }, /** * Convert a time data(time, value) item to (x, y) point. * * @override * @param {Array|number} data data * @param {boolean} [clamp=true] out of range * @return {Array} point */ dataToPoint: function (data, clamp) { isArray(data) && (data = data[0]); clamp == null && (clamp = true); var dayInfo = this.getDateInfo(data); var range = this._rangeInfo; var date = dayInfo.formatedDate; // if not in range return [NaN, NaN] if (clamp && !(dayInfo.time >= range.start.time && dayInfo.time <= range.end.time)) { return [NaN, NaN]; } var week = dayInfo.day; var nthWeek = this._getRangeInfo([range.start.time, date]).nthWeek; if (this._orient === 'vertical') { return [ this._rect.x + week * this._sw + this._sw / 2, this._rect.y + nthWeek * this._sh + this._sh / 2 ]; } return [ this._rect.x + nthWeek * this._sw + this._sw / 2, this._rect.y + week * this._sh + this._sh / 2 ]; }, /** * Convert a (x, y) point to time data * * @override * @param {string} point point * @return {string} data */ pointToData: function (point) { var date = this.pointToDate(point); return date && date.time; }, /** * Convert a time date item to (x, y) four point. * * @param {Array} data date[0] is date * @param {boolean} [clamp=true] out of range * @return {Object} point */ dataToRect: function (data, clamp) { var point = this.dataToPoint(data, clamp); return { contentShape: { x: point[0] - (this._sw - this._lineWidth) / 2, y: point[1] - (this._sh - this._lineWidth) / 2, width: this._sw - this._lineWidth, height: this._sh - this._lineWidth }, center: point, tl: [ point[0] - this._sw / 2, point[1] - this._sh / 2 ], tr: [ point[0] + this._sw / 2, point[1] - this._sh / 2 ], br: [ point[0] + this._sw / 2, point[1] + this._sh / 2 ], bl: [ point[0] - this._sw / 2, point[1] + this._sh / 2 ] }; }, /** * Convert a (x, y) point to time date * * @param {Array} point point * @return {Object} date */ pointToDate: function (point) { var nthX = Math.floor((point[0] - this._rect.x) / this._sw) + 1; var nthY = Math.floor((point[1] - this._rect.y) / this._sh) + 1; var range = this._rangeInfo.range; if (this._orient === 'vertical') { return this._getDateByWeeksAndDay(nthY, nthX - 1, range); } return this._getDateByWeeksAndDay(nthX, nthY - 1, range); }, /** * @inheritDoc */ convertToPixel: curry(doConvert$2, 'dataToPoint'), /** * @inheritDoc */ convertFromPixel: curry(doConvert$2, 'pointToData'), /** * initRange * * @private * @return {Array} [start, end] */ _initRangeOption: function () { var range = this._model.get('range'); var rg = range; if (isArray(rg) && rg.length === 1) { rg = rg[0]; } if (/^\d{4}$/.test(rg)) { range = [rg + '-01-01', rg + '-12-31']; } if (/^\d{4}[\/|-]\d{1,2}$/.test(rg)) { var start = this.getDateInfo(rg); var firstDay = start.date; firstDay.setMonth(firstDay.getMonth() + 1); var end = this.getNextNDay(firstDay, -1); range = [start.formatedDate, end.formatedDate]; } if (/^\d{4}[\/|-]\d{1,2}[\/|-]\d{1,2}$/.test(rg)) { range = [rg, rg]; } var tmp = this._getRangeInfo(range); if (tmp.start.time > tmp.end.time) { range.reverse(); } return range; }, /** * range info * * @private * @param {Array} range range ['2017-01-01', '2017-07-08'] * If range[0] > range[1], they will not be reversed. * @return {Object} obj */ _getRangeInfo: function (range) { range = [ this.getDateInfo(range[0]), this.getDateInfo(range[1]) ]; var reversed; if (range[0].time > range[1].time) { reversed = true; range.reverse(); } var allDay = Math.floor(range[1].time / PROXIMATE_ONE_DAY) - Math.floor(range[0].time / PROXIMATE_ONE_DAY) + 1; // Consider case: // Firstly set system timezone as "Time Zone: America/Toronto", // ``` // var first = new Date(1478412000000 - 3600 * 1000 * 2.5); // var second = new Date(1478412000000); // var allDays = Math.floor(second / ONE_DAY) - Math.floor(first / ONE_DAY) + 1; // ``` // will get wrong result because of DST. So we should fix it. var date = new Date(range[0].time); var startDateNum = date.getDate(); var endDateNum = range[1].date.getDate(); date.setDate(startDateNum + allDay - 1); // The bias can not over a month, so just compare date. if (date.getDate() !== endDateNum) { var sign = date.getTime() - range[1].time > 0 ? 1 : -1; while (date.getDate() !== endDateNum && (date.getTime() - range[1].time) * sign > 0) { allDay -= sign; date.setDate(startDateNum + allDay - 1); } } var weeks = Math.floor((allDay + range[0].day + 6) / 7); var nthWeek = reversed ? -weeks + 1: weeks - 1; reversed && range.reverse(); return { range: [range[0].formatedDate, range[1].formatedDate], start: range[0], end: range[1], allDay: allDay, weeks: weeks, // From 0. nthWeek: nthWeek, fweek: range[0].day, lweek: range[1].day }; }, /** * get date by nthWeeks and week day in range * * @private * @param {number} nthWeek the week * @param {number} day the week day * @param {Array} range [d1, d2] * @return {Object} */ _getDateByWeeksAndDay: function (nthWeek, day, range) { var rangeInfo = this._getRangeInfo(range); if (nthWeek > rangeInfo.weeks || (nthWeek === 0 && day < rangeInfo.fweek) || (nthWeek === rangeInfo.weeks && day > rangeInfo.lweek) ) { return false; } var nthDay = (nthWeek - 1) * 7 - rangeInfo.fweek + day; var date = new Date(rangeInfo.start.time); date.setDate(rangeInfo.start.d + nthDay); return this.getDateInfo(date); } }; Calendar.dimensions = Calendar.prototype.dimensions; Calendar.getDimensionsInfo = Calendar.prototype.getDimensionsInfo; Calendar.create = function (ecModel, api) { var calendarList = []; ecModel.eachComponent('calendar', function (calendarModel) { var calendar = new Calendar(calendarModel, ecModel, api); calendarList.push(calendar); calendarModel.coordinateSystem = calendar; }); ecModel.eachSeries(function (calendarSeries) { if (calendarSeries.get('coordinateSystem') === 'calendar') { // Inject coordinate system calendarSeries.coordinateSystem = calendarList[calendarSeries.get('calendarIndex') || 0]; } }); return calendarList; }; function doConvert$2(methodName, ecModel, finder, value) { var calendarModel = finder.calendarModel; var seriesModel = finder.seriesModel; var coordSys = calendarModel ? calendarModel.coordinateSystem : seriesModel ? seriesModel.coordinateSystem : null; return coordSys === this ? coordSys[methodName](value) : null; } CoordinateSystemManager.register('calendar', Calendar); var CalendarModel = ComponentModel.extend({ type: 'calendar', /** * @type {module:echarts/coord/calendar/Calendar} */ coordinateSystem: null, defaultOption: { zlevel: 0, z: 2, left: 80, top: 60, cellSize: 20, // horizontal vertical orient: 'horizontal', // month separate line style splitLine: { show: true, lineStyle: { color: '#000', width: 1, type: 'solid' } }, // rect style temporarily unused emphasis itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#ccc' }, // week text style dayLabel: { show: true, // a week first day firstDay: 0, // start end position: 'start', margin: '50%', // 50% of cellSize nameMap: 'en', color: '#000' }, // month text style monthLabel: { show: true, // start end position: 'start', margin: 5, // center or left align: 'center', // cn en [] nameMap: 'en', formatter: null, color: '#000' }, // year text style yearLabel: { show: true, // top bottom left right position: null, margin: 30, formatter: null, color: '#ccc', fontFamily: 'sans-serif', fontWeight: 'bolder', fontSize: 20 } }, /** * @override */ init: function (option, parentModel, ecModel, extraOpt) { var inputPositionParams = getLayoutParams(option); CalendarModel.superApply(this, 'init', arguments); mergeAndNormalizeLayoutParams$1(option, inputPositionParams); }, /** * @override */ mergeOption: function (option, extraOpt) { CalendarModel.superApply(this, 'mergeOption', arguments); mergeAndNormalizeLayoutParams$1(this.option, option); } }); function mergeAndNormalizeLayoutParams$1(target, raw) { // Normalize cellSize var cellSize = target.cellSize; if (!isArray(cellSize)) { cellSize = target.cellSize = [cellSize, cellSize]; } else if (cellSize.length === 1) { cellSize[1] = cellSize[0]; } var ignoreSize = map([0, 1], function (hvIdx) { // If user have set `width` or both `left` and `right`, cellSize // will be automatically set to 'auto', otherwise the default // setting of cellSize will make `width` setting not work. if (sizeCalculable(raw, hvIdx)) { cellSize[hvIdx] = 'auto'; } return cellSize[hvIdx] != null && cellSize[hvIdx] !== 'auto'; }); mergeLayoutParam(target, raw, { type: 'box', ignoreSize: ignoreSize }); } var MONTH_TEXT = { EN: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], CN: [ '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' ] }; var WEEK_TEXT = { EN: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], CN: ['日', '一', '二', '三', '四', '五', '六'] }; extendComponentView({ type: 'calendar', /** * top/left line points * @private */ _tlpoints: null, /** * bottom/right line points * @private */ _blpoints: null, /** * first day of month * @private */ _firstDayOfMonth: null, /** * first day point of month * @private */ _firstDayPoints: null, render: function (calendarModel, ecModel, api) { var group = this.group; group.removeAll(); var coordSys = calendarModel.coordinateSystem; // range info var rangeData = coordSys.getRangeInfo(); var orient = coordSys.getOrient(); this._renderDayRect(calendarModel, rangeData, group); // _renderLines must be called prior to following function this._renderLines(calendarModel, rangeData, orient, group); this._renderYearText(calendarModel, rangeData, orient, group); this._renderMonthText(calendarModel, orient, group); this._renderWeekText(calendarModel, rangeData, orient, group); }, // render day rect _renderDayRect: function (calendarModel, rangeData, group) { var coordSys = calendarModel.coordinateSystem; var itemRectStyleModel = calendarModel.getModel('itemStyle').getItemStyle(); var sw = coordSys.getCellWidth(); var sh = coordSys.getCellHeight(); for (var i = rangeData.start.time; i <= rangeData.end.time; i = coordSys.getNextNDay(i, 1).time ) { var point = coordSys.dataToRect([i], false).tl; // every rect var rect = new Rect({ shape: { x: point[0], y: point[1], width: sw, height: sh }, cursor: 'default', style: itemRectStyleModel }); group.add(rect); } }, // render separate line _renderLines: function (calendarModel, rangeData, orient, group) { var self = this; var coordSys = calendarModel.coordinateSystem; var lineStyleModel = calendarModel.getModel('splitLine.lineStyle').getLineStyle(); var show = calendarModel.get('splitLine.show'); var lineWidth = lineStyleModel.lineWidth; this._tlpoints = []; this._blpoints = []; this._firstDayOfMonth = []; this._firstDayPoints = []; var firstDay = rangeData.start; for (var i = 0; firstDay.time <= rangeData.end.time; i++) { addPoints(firstDay.formatedDate); if (i === 0) { firstDay = coordSys.getDateInfo(rangeData.start.y + '-' + rangeData.start.m); } var date = firstDay.date; date.setMonth(date.getMonth() + 1); firstDay = coordSys.getDateInfo(date); } addPoints(coordSys.getNextNDay(rangeData.end.time, 1).formatedDate); function addPoints(date) { self._firstDayOfMonth.push(coordSys.getDateInfo(date)); self._firstDayPoints.push(coordSys.dataToRect([date], false).tl); var points = self._getLinePointsOfOneWeek(calendarModel, date, orient); self._tlpoints.push(points[0]); self._blpoints.push(points[points.length - 1]); show && self._drawSplitline(points, lineStyleModel, group); } // render top/left line show && this._drawSplitline(self._getEdgesPoints(self._tlpoints, lineWidth, orient), lineStyleModel, group); // render bottom/right line show && this._drawSplitline(self._getEdgesPoints(self._blpoints, lineWidth, orient), lineStyleModel, group); }, // get points at both ends _getEdgesPoints: function (points, lineWidth, orient) { var rs = [points[0].slice(), points[points.length - 1].slice()]; var idx = orient === 'horizontal' ? 0 : 1; // both ends of the line are extend half lineWidth rs[0][idx] = rs[0][idx] - lineWidth / 2; rs[1][idx] = rs[1][idx] + lineWidth / 2; return rs; }, // render split line _drawSplitline: function (points, lineStyleModel, group) { var poyline = new Polyline({ z2: 20, shape: { points: points }, style: lineStyleModel }); group.add(poyline); }, // render month line of one week points _getLinePointsOfOneWeek: function (calendarModel, date, orient) { var coordSys = calendarModel.coordinateSystem; date = coordSys.getDateInfo(date); var points = []; for (var i = 0; i < 7; i++) { var tmpD = coordSys.getNextNDay(date.time, i); var point = coordSys.dataToRect([tmpD.time], false); points[2 * tmpD.day] = point.tl; points[2 * tmpD.day + 1] = point[orient === 'horizontal' ? 'bl' : 'tr']; } return points; }, _formatterLabel: function (formatter, params) { if (typeof formatter === 'string' && formatter) { return formatTplSimple(formatter, params); } if (typeof formatter === 'function') { return formatter(params); } return params.nameMap; }, _yearTextPositionControl: function (textEl, point, orient, position, margin) { point = point.slice(); var aligns = ['center', 'bottom']; if (position === 'bottom') { point[1] += margin; aligns = ['center', 'top']; } else if (position === 'left') { point[0] -= margin; } else if (position === 'right') { point[0] += margin; aligns = ['center', 'top']; } else { // top point[1] -= margin; } var rotate = 0; if (position === 'left' || position === 'right') { rotate = Math.PI / 2; } return { rotation: rotate, position: point, style: { textAlign: aligns[0], textVerticalAlign: aligns[1] } }; }, // render year _renderYearText: function (calendarModel, rangeData, orient, group) { var yearLabel = calendarModel.getModel('yearLabel'); if (!yearLabel.get('show')) { return; } var margin = yearLabel.get('margin'); var pos = yearLabel.get('position'); if (!pos) { pos = orient !== 'horizontal' ? 'top' : 'left'; } var points = [this._tlpoints[this._tlpoints.length - 1], this._blpoints[0]]; var xc = (points[0][0] + points[1][0]) / 2; var yc = (points[0][1] + points[1][1]) / 2; var idx = orient === 'horizontal' ? 0 : 1; var posPoints = { top: [xc, points[idx][1]], bottom: [xc, points[1 - idx][1]], left: [points[1 - idx][0], yc], right: [points[idx][0], yc] }; var name = rangeData.start.y; if (+rangeData.end.y > +rangeData.start.y) { name = name + '-' + rangeData.end.y; } var formatter = yearLabel.get('formatter'); var params = { start: rangeData.start.y, end: rangeData.end.y, nameMap: name }; var content = this._formatterLabel(formatter, params); var yearText = new Text({z2: 30}); setTextStyle(yearText.style, yearLabel, {text: content}), yearText.attr(this._yearTextPositionControl(yearText, posPoints[pos], orient, pos, margin)); group.add(yearText); }, _monthTextPositionControl: function (point, isCenter, orient, position, margin) { var align = 'left'; var vAlign = 'top'; var x = point[0]; var y = point[1]; if (orient === 'horizontal') { y = y + margin; if (isCenter) { align = 'center'; } if (position === 'start') { vAlign = 'bottom'; } } else { x = x + margin; if (isCenter) { vAlign = 'middle'; } if (position === 'start') { align = 'right'; } } return { x: x, y: y, textAlign: align, textVerticalAlign: vAlign }; }, // render month and year text _renderMonthText: function (calendarModel, orient, group) { var monthLabel = calendarModel.getModel('monthLabel'); if (!monthLabel.get('show')) { return; } var nameMap = monthLabel.get('nameMap'); var margin = monthLabel.get('margin'); var pos = monthLabel.get('position'); var align = monthLabel.get('align'); var termPoints = [this._tlpoints, this._blpoints]; if (isString(nameMap)) { nameMap = MONTH_TEXT[nameMap.toUpperCase()] || []; } var idx = pos === 'start' ? 0 : 1; var axis = orient === 'horizontal' ? 0 : 1; margin = pos === 'start' ? -margin : margin; var isCenter = (align === 'center'); for (var i = 0; i < termPoints[idx].length - 1; i++) { var tmp = termPoints[idx][i].slice(); var firstDay = this._firstDayOfMonth[i]; if (isCenter) { var firstDayPoints = this._firstDayPoints[i]; tmp[axis] = (firstDayPoints[axis] + termPoints[0][i + 1][axis]) / 2; } var formatter = monthLabel.get('formatter'); var name = nameMap[+firstDay.m - 1]; var params = { yyyy: firstDay.y, yy: (firstDay.y + '').slice(2), MM: firstDay.m, M: +firstDay.m, nameMap: name }; var content = this._formatterLabel(formatter, params); var monthText = new Text({z2: 30}); extend( setTextStyle(monthText.style, monthLabel, {text: content}), this._monthTextPositionControl(tmp, isCenter, orient, pos, margin) ); group.add(monthText); } }, _weekTextPositionControl: function (point, orient, position, margin, cellSize) { var align = 'center'; var vAlign = 'middle'; var x = point[0]; var y = point[1]; var isStart = position === 'start'; if (orient === 'horizontal') { x = x + margin + (isStart ? 1 : -1) * cellSize[0] / 2; align = isStart ? 'right' : 'left'; } else { y = y + margin + (isStart ? 1 : -1) * cellSize[1] / 2; vAlign = isStart ? 'bottom' : 'top'; } return { x: x, y: y, textAlign: align, textVerticalAlign: vAlign }; }, // render weeks _renderWeekText: function (calendarModel, rangeData, orient, group) { var dayLabel = calendarModel.getModel('dayLabel'); if (!dayLabel.get('show')) { return; } var coordSys = calendarModel.coordinateSystem; var pos = dayLabel.get('position'); var nameMap = dayLabel.get('nameMap'); var margin = dayLabel.get('margin'); var firstDayOfWeek = coordSys.getFirstDayOfWeek(); if (isString(nameMap)) { nameMap = WEEK_TEXT[nameMap.toUpperCase()] || []; } var start = coordSys.getNextNDay( rangeData.end.time, (7 - rangeData.lweek) ).time; var cellSize = [coordSys.getCellWidth(), coordSys.getCellHeight()]; margin = parsePercent$1(margin, cellSize[orient === 'horizontal' ? 0 : 1]); if (pos === 'start') { start = coordSys.getNextNDay( rangeData.start.time, -(7 + rangeData.fweek) ).time; margin = -margin; } for (var i = 0; i < 7; i++) { var tmpD = coordSys.getNextNDay(start, i); var point = coordSys.dataToRect([tmpD.time], false).center; var day = i; day = Math.abs((i + firstDayOfWeek) % 7); var weekText = new Text({z2: 30}); extend( setTextStyle(weekText.style, dayLabel, {text: nameMap[day]}), this._weekTextPositionControl(point, orient, pos, margin, cellSize) ); group.add(weekText); } } }); /** * @file calendar.js * @author dxh */ // Model extendComponentModel({ type: 'title', layoutMode: {type: 'box', ignoreSize: true}, defaultOption: { // 一级层叠 zlevel: 0, // 二级层叠 z: 6, show: true, text: '', // 超链接跳转 // link: null, // 仅支持self | blank target: 'blank', subtext: '', // 超链接跳转 // sublink: null, // 仅支持self | blank subtarget: 'blank', // 'center' ¦ 'left' ¦ 'right' // ¦ {number}(x坐标,单位px) left: 0, // 'top' ¦ 'bottom' ¦ 'center' // ¦ {number}(y坐标,单位px) top: 0, // 水平对齐 // 'auto' | 'left' | 'right' | 'center' // 默认根据 left 的位置判断是左对齐还是右对齐 // textAlign: null // // 垂直对齐 // 'auto' | 'top' | 'bottom' | 'middle' // 默认根据 top 位置判断是上对齐还是下对齐 // textBaseline: null backgroundColor: 'rgba(0,0,0,0)', // 标题边框颜色 borderColor: '#ccc', // 标题边框线宽,单位px,默认为0(无边框) borderWidth: 0, // 标题内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css padding: 5, // 主副标题纵向间隔,单位px,默认为10, itemGap: 10, textStyle: { fontSize: 18, fontWeight: 'bolder', color: '#333' }, subtextStyle: { color: '#aaa' } } }); // View extendComponentView({ type: 'title', render: function (titleModel, ecModel, api) { this.group.removeAll(); if (!titleModel.get('show')) { return; } var group = this.group; var textStyleModel = titleModel.getModel('textStyle'); var subtextStyleModel = titleModel.getModel('subtextStyle'); var textAlign = titleModel.get('textAlign'); var textBaseline = titleModel.get('textBaseline'); var textEl = new Text({ style: setTextStyle({}, textStyleModel, { text: titleModel.get('text'), textFill: textStyleModel.getTextColor() }, {disableBox: true}), z2: 10 }); var textRect = textEl.getBoundingRect(); var subText = titleModel.get('subtext'); var subTextEl = new Text({ style: setTextStyle({}, subtextStyleModel, { text: subText, textFill: subtextStyleModel.getTextColor(), y: textRect.height + titleModel.get('itemGap'), textVerticalAlign: 'top' }, {disableBox: true}), z2: 10 }); var link = titleModel.get('link'); var sublink = titleModel.get('sublink'); textEl.silent = !link; subTextEl.silent = !sublink; if (link) { textEl.on('click', function () { window.open(link, '_' + titleModel.get('target')); }); } if (sublink) { subTextEl.on('click', function () { window.open(sublink, '_' + titleModel.get('subtarget')); }); } group.add(textEl); subText && group.add(subTextEl); // If no subText, but add subTextEl, there will be an empty line. var groupRect = group.getBoundingRect(); var layoutOption = titleModel.getBoxLayoutParams(); layoutOption.width = groupRect.width; layoutOption.height = groupRect.height; var layoutRect = getLayoutRect( layoutOption, { width: api.getWidth(), height: api.getHeight() }, titleModel.get('padding') ); // Adjust text align based on position if (!textAlign) { // Align left if title is on the left. center and right is same textAlign = titleModel.get('left') || titleModel.get('right'); if (textAlign === 'middle') { textAlign = 'center'; } // Adjust layout by text align if (textAlign === 'right') { layoutRect.x += layoutRect.width; } else if (textAlign === 'center') { layoutRect.x += layoutRect.width / 2; } } if (!textBaseline) { textBaseline = titleModel.get('top') || titleModel.get('bottom'); if (textBaseline === 'center') { textBaseline = 'middle'; } if (textBaseline === 'bottom') { layoutRect.y += layoutRect.height; } else if (textBaseline === 'middle') { layoutRect.y += layoutRect.height / 2; } textBaseline = textBaseline || 'top'; } group.attr('position', [layoutRect.x, layoutRect.y]); var alignStyle = { textAlign: textAlign, textVerticalAlign: textBaseline }; textEl.setStyle(alignStyle); subTextEl.setStyle(alignStyle); // Render background // Get groupRect again because textAlign has been changed groupRect = group.getBoundingRect(); var padding = layoutRect.margin; var style = titleModel.getItemStyle(['color', 'opacity']); style.fill = titleModel.get('backgroundColor'); var rect = new Rect({ shape: { x: groupRect.x - padding[3], y: groupRect.y - padding[0], width: groupRect.width + padding[1] + padding[3], height: groupRect.height + padding[0] + padding[2], r: titleModel.get('borderRadius') }, style: style, silent: true }); subPixelOptimizeRect(rect); group.add(rect); } }); ComponentModel.registerSubTypeDefaulter('dataZoom', function () { // Default 'slider' when no type specified. return 'slider'; }); var AXIS_DIMS = ['x', 'y', 'z', 'radius', 'angle', 'single']; // Supported coords. var COORDS = ['cartesian2d', 'polar', 'singleAxis']; /** * @param {string} coordType * @return {boolean} */ function isCoordSupported(coordType) { return indexOf(COORDS, coordType) >= 0; } /** * Create "each" method to iterate names. * * @pubilc * @param {Array.<string>} names * @param {Array.<string>=} attrs * @return {Function} */ function createNameEach(names, attrs) { names = names.slice(); var capitalNames = map(names, capitalFirst); attrs = (attrs || []).slice(); var capitalAttrs = map(attrs, capitalFirst); return function (callback, context) { each$1(names, function (name, index) { var nameObj = {name: name, capital: capitalNames[index]}; for (var j = 0; j < attrs.length; j++) { nameObj[attrs[j]] = name + capitalAttrs[j]; } callback.call(context, nameObj); }); }; } /** * Iterate each dimension name. * * @public * @param {Function} callback The parameter is like: * { * name: 'angle', * capital: 'Angle', * axis: 'angleAxis', * axisIndex: 'angleAixs', * index: 'angleIndex' * } * @param {Object} context */ var eachAxisDim$1 = createNameEach(AXIS_DIMS, ['axisIndex', 'axis', 'index', 'id']); /** * If tow dataZoomModels has the same axis controlled, we say that they are 'linked'. * dataZoomModels and 'links' make up one or more graphics. * This function finds the graphic where the source dataZoomModel is in. * * @public * @param {Function} forEachNode Node iterator. * @param {Function} forEachEdgeType edgeType iterator * @param {Function} edgeIdGetter Giving node and edgeType, return an array of edge id. * @return {Function} Input: sourceNode, Output: Like {nodes: [], dims: {}} */ function createLinkedNodesFinder(forEachNode, forEachEdgeType, edgeIdGetter) { return function (sourceNode) { var result = { nodes: [], records: {} // key: edgeType.name, value: Object (key: edge id, value: boolean). }; forEachEdgeType(function (edgeType) { result.records[edgeType.name] = {}; }); if (!sourceNode) { return result; } absorb(sourceNode, result); var existsLink; do { existsLink = false; forEachNode(processSingleNode); } while (existsLink); function processSingleNode(node) { if (!isNodeAbsorded(node, result) && isLinked(node, result)) { absorb(node, result); existsLink = true; } } return result; }; function isNodeAbsorded(node, result) { return indexOf(result.nodes, node) >= 0; } function isLinked(node, result) { var hasLink = false; forEachEdgeType(function (edgeType) { each$1(edgeIdGetter(node, edgeType) || [], function (edgeId) { result.records[edgeType.name][edgeId] && (hasLink = true); }); }); return hasLink; } function absorb(node, result) { result.nodes.push(node); forEachEdgeType(function (edgeType) { each$1(edgeIdGetter(node, edgeType) || [], function (edgeId) { result.records[edgeType.name][edgeId] = true; }); }); } } var each$23 = each$1; var asc$1 = asc; /** * Operate single axis. * One axis can only operated by one axis operator. * Different dataZoomModels may be defined to operate the same axis. * (i.e. 'inside' data zoom and 'slider' data zoom components) * So dataZoomModels share one axisProxy in that case. * * @class */ var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) { /** * @private * @type {string} */ this._dimName = dimName; /** * @private */ this._axisIndex = axisIndex; /** * @private * @type {Array.<number>} */ this._valueWindow; /** * @private * @type {Array.<number>} */ this._percentWindow; /** * @private * @type {Array.<number>} */ this._dataExtent; /** * {minSpan, maxSpan, minValueSpan, maxValueSpan} * @private * @type {Object} */ this._minMaxSpan; /** * @readOnly * @type {module: echarts/model/Global} */ this.ecModel = ecModel; /** * @private * @type {module: echarts/component/dataZoom/DataZoomModel} */ this._dataZoomModel = dataZoomModel; // /** // * @readOnly // * @private // */ // this.hasSeriesStacked; }; AxisProxy.prototype = { constructor: AxisProxy, /** * Whether the axisProxy is hosted by dataZoomModel. * * @public * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel * @return {boolean} */ hostedBy: function (dataZoomModel) { return this._dataZoomModel === dataZoomModel; }, /** * @return {Array.<number>} Value can only be NaN or finite value. */ getDataValueWindow: function () { return this._valueWindow.slice(); }, /** * @return {Array.<number>} */ getDataPercentWindow: function () { return this._percentWindow.slice(); }, /** * @public * @param {number} axisIndex * @return {Array} seriesModels */ getTargetSeriesModels: function () { var seriesModels = []; var ecModel = this.ecModel; ecModel.eachSeries(function (seriesModel) { if (isCoordSupported(seriesModel.get('coordinateSystem'))) { var dimName = this._dimName; var axisModel = ecModel.queryComponents({ mainType: dimName + 'Axis', index: seriesModel.get(dimName + 'AxisIndex'), id: seriesModel.get(dimName + 'AxisId') })[0]; if (this._axisIndex === (axisModel && axisModel.componentIndex)) { seriesModels.push(seriesModel); } } }, this); return seriesModels; }, getAxisModel: function () { return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex); }, getOtherAxisModel: function () { var axisDim = this._dimName; var ecModel = this.ecModel; var axisModel = this.getAxisModel(); var isCartesian = axisDim === 'x' || axisDim === 'y'; var otherAxisDim; var coordSysIndexName; if (isCartesian) { coordSysIndexName = 'gridIndex'; otherAxisDim = axisDim === 'x' ? 'y' : 'x'; } else { coordSysIndexName = 'polarIndex'; otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle'; } var foundOtherAxisModel; ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) { if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0) ) { foundOtherAxisModel = otherAxisModel; } }); return foundOtherAxisModel; }, getMinMaxSpan: function () { return clone(this._minMaxSpan); }, /** * Only calculate by given range and this._dataExtent, do not change anything. * * @param {Object} opt * @param {number} [opt.start] * @param {number} [opt.end] * @param {number} [opt.startValue] * @param {number} [opt.endValue] */ calculateDataWindow: function (opt) { var dataExtent = this._dataExtent; var axisModel = this.getAxisModel(); var scale = axisModel.axis.scale; var rangePropMode = this._dataZoomModel.getRangePropMode(); var percentExtent = [0, 100]; var percentWindow = [ opt.start, opt.end ]; var valueWindow = []; each$23(['startValue', 'endValue'], function (prop) { valueWindow.push(opt[prop] != null ? scale.parse(opt[prop]) : null); }); // Normalize bound. each$23([0, 1], function (idx) { var boundValue = valueWindow[idx]; var boundPercent = percentWindow[idx]; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or // on `valueProp` ('startValue', 'endValue'). The former one is suitable // for cases that a dataZoom component controls multiple axes with different // unit or extent, and the latter one is suitable for accurate zoom by pixel // (e.g., in dataZoomSelect). `valueProp` can be calculated from `percentProp`, // but it is awkward that `percentProp` can not be obtained from `valueProp` // accurately (because all of values that are overflow the `dataExtent` will // be calculated to percent '100%'). So we have to use // `dataZoom.getRangePropMode()` to mark which prop is used. // `rangePropMode` is updated only when setOption or dispatchAction, otherwise // it remains its original value. if (rangePropMode[idx] === 'percent') { if (boundPercent == null) { boundPercent = percentExtent[idx]; } // Use scale.parse to math round for category or time axis. boundValue = scale.parse(linearMap( boundPercent, percentExtent, dataExtent, true )); } else { // Calculating `percent` from `value` may be not accurate, because // This calculation can not be inversed, because all of values that // are overflow the `dataExtent` will be calculated to percent '100%' boundPercent = linearMap( boundValue, dataExtent, percentExtent, true ); } // valueWindow[idx] = round(boundValue); // percentWindow[idx] = round(boundPercent); valueWindow[idx] = boundValue; percentWindow[idx] = boundPercent; }); return { valueWindow: asc$1(valueWindow), percentWindow: asc$1(percentWindow) }; }, /** * Notice: reset should not be called before series.restoreData() called, * so it is recommanded to be called in "process stage" but not "model init * stage". * * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ reset: function (dataZoomModel) { if (dataZoomModel !== this._dataZoomModel) { return; } var targetSeries = this.getTargetSeriesModels(); // Culculate data window and data extent, and record them. this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); // this.hasSeriesStacked = false; // each(targetSeries, function (series) { // var data = series.getData(); // var dataDim = data.mapDimension(this._dimName); // var stackedDimension = data.getCalculationInfo('stackedDimension'); // if (stackedDimension && stackedDimension === dataDim) { // this.hasSeriesStacked = true; // } // }, this); var dataWindow = this.calculateDataWindow(dataZoomModel.option); this._valueWindow = dataWindow.valueWindow; this._percentWindow = dataWindow.percentWindow; setMinMaxSpan(this); // Update axis setting then. setAxisModel(this); }, /** * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ restore: function (dataZoomModel) { if (dataZoomModel !== this._dataZoomModel) { return; } this._valueWindow = this._percentWindow = null; setAxisModel(this, true); }, /** * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel */ filterData: function (dataZoomModel, api) { if (dataZoomModel !== this._dataZoomModel) { return; } var axisDim = this._dimName; var seriesModels = this.getTargetSeriesModels(); var filterMode = dataZoomModel.get('filterMode'); var valueWindow = this._valueWindow; if (filterMode === 'none') { return; } // FIXME // Toolbox may has dataZoom injected. And if there are stacked bar chart // with NaN data, NaN will be filtered and stack will be wrong. // So we need to force the mode to be set empty. // In fect, it is not a big deal that do not support filterMode-'filter' // when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis // selection" some day, which might need "adapt to data extent on the // otherAxis", which is disabled by filterMode-'empty'. // But currently, stack has been fixed to based on value but not index, // so this is not an issue any more. // var otherAxisModel = this.getOtherAxisModel(); // if (dataZoomModel.get('$fromToolbox') // && otherAxisModel // && otherAxisModel.hasSeriesStacked // ) { // filterMode = 'empty'; // } // TODO // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet. // Process series data each$23(seriesModels, function (seriesModel) { var seriesData = seriesModel.getData(); var dataDims = seriesData.mapDimension(axisDim, true); if (filterMode === 'weakFilter') { seriesData.filterSelf(function (dataIndex) { var leftOut; var rightOut; var hasValue; for (var i = 0; i < dataDims.length; i++) { var value = seriesData.get(dataDims[i], dataIndex); var thisHasValue = !isNaN(value); var thisLeftOut = value < valueWindow[0]; var thisRightOut = value > valueWindow[1]; if (thisHasValue && !thisLeftOut && !thisRightOut) { return true; } thisHasValue && (hasValue = true); thisLeftOut && (leftOut = true); thisRightOut && (rightOut = true); } // If both left out and right out, do not filter. return hasValue && leftOut && rightOut; }); } else { each$23(dataDims, function (dim) { if (filterMode === 'empty') { seriesModel.setData( seriesData.map(dim, function (value) { return !isInWindow(value) ? NaN : value; }) ); } else { var range = {}; range[dim] = valueWindow; // console.time('select'); seriesData.selectRange(range); // console.timeEnd('select'); } }); } each$23(dataDims, function (dim) { seriesData.setApproximateExtent(valueWindow, dim); }); }); function isInWindow(value) { return value >= valueWindow[0] && value <= valueWindow[1]; } } }; function calculateDataExtent(axisProxy, axisDim, seriesModels) { var dataExtent = [Infinity, -Infinity]; each$23(seriesModels, function (seriesModel) { var seriesData = seriesModel.getData(); if (seriesData) { each$23(seriesData.mapDimension(axisDim, true), function (dim) { var seriesExtent = seriesData.getApproximateExtent(dim); seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]); seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]); }); } }); if (dataExtent[1] < dataExtent[0]) { dataExtent = [NaN, NaN]; } // It is important to get "consistent" extent when more then one axes is // controlled by a `dataZoom`, otherwise those axes will not be synchronized // when zooming. But it is difficult to know what is "consistent", considering // axes have different type or even different meanings (For example, two // time axes are used to compare data of the same date in different years). // So basically dataZoom just obtains extent by series.data (in category axis // extent can be obtained from axis.data). // Nevertheless, user can set min/max/scale on axes to make extent of axes // consistent. fixExtentByAxis(axisProxy, dataExtent); return dataExtent; } function fixExtentByAxis(axisProxy, dataExtent) { var axisModel = axisProxy.getAxisModel(); var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined // by axis.data by default. var isCategoryAxis = axisModel.get('type') === 'category'; var axisDataLen = isCategoryAxis && axisModel.getCategories().length; if (min != null && min !== 'dataMin' && typeof min !== 'function') { dataExtent[0] = min; } else if (isCategoryAxis) { dataExtent[0] = axisDataLen > 0 ? 0 : NaN; } var max = axisModel.getMax(true); if (max != null && max !== 'dataMax' && typeof max !== 'function') { dataExtent[1] = max; } else if (isCategoryAxis) { dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN; } if (!axisModel.get('scale', true)) { dataExtent[0] > 0 && (dataExtent[0] = 0); dataExtent[1] < 0 && (dataExtent[1] = 0); } // For value axis, if min/max/scale are not set, we just use the extent obtained // by series data, which may be a little different from the extent calculated by // `axisHelper.getScaleExtent`. But the different just affects the experience a // little when zooming. So it will not be fixed until some users require it strongly. return dataExtent; } function setAxisModel(axisProxy, isRestore) { var axisModel = axisProxy.getAxisModel(); var percentWindow = axisProxy._percentWindow; var valueWindow = axisProxy._valueWindow; if (!percentWindow) { return; } // [0, 500]: arbitrary value, guess axis extent. var precision = getPixelPrecision(valueWindow, [0, 500]); precision = Math.min(precision, 20); // isRestore or isFull var useOrigin = isRestore || (percentWindow[0] === 0 && percentWindow[1] === 100); axisModel.setRange( useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision) ); } function setMinMaxSpan(axisProxy) { var minMaxSpan = axisProxy._minMaxSpan = {}; var dataZoomModel = axisProxy._dataZoomModel; each$23(['min', 'max'], function (minMax) { minMaxSpan[minMax + 'Span'] = dataZoomModel.get(minMax + 'Span'); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan var valueSpan = dataZoomModel.get(minMax + 'ValueSpan'); if (valueSpan != null) { minMaxSpan[minMax + 'ValueSpan'] = valueSpan; valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan); if (valueSpan != null) { var dataExtent = axisProxy._dataExtent; minMaxSpan[minMax + 'Span'] = linearMap( dataExtent[0] + valueSpan, dataExtent, [0, 100], true ); } } }); } var each$22 = each$1; var eachAxisDim = eachAxisDim$1; var DataZoomModel = extendComponentModel({ type: 'dataZoom', dependencies: [ 'xAxis', 'yAxis', 'zAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series' ], /** * @protected */ defaultOption: { zlevel: 0, z: 4, // Higher than normal component (z: 2). orient: null, // Default auto by axisIndex. Possible value: 'horizontal', 'vertical'. xAxisIndex: null, // Default the first horizontal category axis. yAxisIndex: null, // Default the first vertical category axis. filterMode: 'filter', // Possible values: 'filter' or 'empty' or 'weakFilter'. // 'filter': data items which are out of window will be removed. This option is // applicable when filtering outliers. For each data item, it will be // filtered if one of the relevant dimensions is out of the window. // 'weakFilter': data items which are out of window will be removed. This option // is applicable when filtering outliers. For each data item, it will be // filtered only if all of the relevant dimensions are out of the same // side of the window. // 'empty': data items which are out of window will be set to empty. // This option is applicable when user should not neglect // that there are some data items out of window. // 'none': Do not filter. // Taking line chart as an example, line will be broken in // the filtered points when filterModel is set to 'empty', but // be connected when set to 'filter'. throttle: null, // Dispatch action by the fixed rate, avoid frequency. // default 100. Do not throttle when use null/undefined. // If animation === true and animationDurationUpdate > 0, // default value is 100, otherwise 20. start: 0, // Start percent. 0 ~ 100 end: 100, // End percent. 0 ~ 100 startValue: null, // Start value. If startValue specified, start is ignored. endValue: null, // End value. If endValue specified, end is ignored. minSpan: null, // 0 ~ 100 maxSpan: null, // 0 ~ 100 minValueSpan: null, // The range of dataZoom can not be smaller than that. maxValueSpan: null, // The range of dataZoom can not be larger than that. rangeMode: null // Array, can be 'value' or 'percent'. }, /** * @override */ init: function (option, parentModel, ecModel) { /** * key like x_0, y_1 * @private * @type {Object} */ this._dataIntervalByAxis = {}; /** * @private */ this._dataInfo = {}; /** * key like x_0, y_1 * @private */ this._axisProxies = {}; /** * @readOnly */ this.textStyleModel; /** * @private */ this._autoThrottle = true; /** * 'percent' or 'value' * @private */ this._rangePropMode = ['percent', 'percent']; var rawOption = retrieveRaw(option); this.mergeDefaultAndTheme(option, ecModel); this.doInit(rawOption); }, /** * @override */ mergeOption: function (newOption) { var rawOption = retrieveRaw(newOption); //FIX #2591 merge(this.option, newOption, true); this.doInit(rawOption); }, /** * @protected */ doInit: function (rawOption) { var thisOption = this.option; // Disable realtime view update if canvas is not supported. if (!env$1.canvasSupported) { thisOption.realtime = false; } this._setDefaultThrottle(rawOption); updateRangeUse(this, rawOption); each$22([['start', 'startValue'], ['end', 'endValue']], function (names, index) { // start/end has higher priority over startValue/endValue if they // both set, but we should make chart.setOption({endValue: 1000}) // effective, rather than chart.setOption({endValue: 1000, end: null}). if (this._rangePropMode[index] === 'value') { thisOption[names[0]] = null; } // Otherwise do nothing and use the merge result. }, this); this.textStyleModel = this.getModel('textStyle'); this._resetTarget(); this._giveAxisProxies(); }, /** * @private */ _giveAxisProxies: function () { var axisProxies = this._axisProxies; this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) { var axisModel = this.dependentModels[dimNames.axis][axisIndex]; // If exists, share axisProxy with other dataZoomModels. var axisProxy = axisModel.__dzAxisProxy || ( // Use the first dataZoomModel as the main model of axisProxy. axisModel.__dzAxisProxy = new AxisProxy( dimNames.name, axisIndex, this, ecModel ) ); // FIXME // dispose __dzAxisProxy axisProxies[dimNames.name + '_' + axisIndex] = axisProxy; }, this); }, /** * @private */ _resetTarget: function () { var thisOption = this.option; var autoMode = this._judgeAutoMode(); eachAxisDim(function (dimNames) { var axisIndexName = dimNames.axisIndex; thisOption[axisIndexName] = normalizeToArray( thisOption[axisIndexName] ); }, this); if (autoMode === 'axisIndex') { this._autoSetAxisIndex(); } else if (autoMode === 'orient') { this._autoSetOrient(); } }, /** * @private */ _judgeAutoMode: function () { // Auto set only works for setOption at the first time. // The following is user's reponsibility. So using merged // option is OK. var thisOption = this.option; var hasIndexSpecified = false; eachAxisDim(function (dimNames) { // When user set axisIndex as a empty array, we think that user specify axisIndex // but do not want use auto mode. Because empty array may be encountered when // some error occured. if (thisOption[dimNames.axisIndex] != null) { hasIndexSpecified = true; } }, this); var orient = thisOption.orient; if (orient == null && hasIndexSpecified) { return 'orient'; } else if (!hasIndexSpecified) { if (orient == null) { thisOption.orient = 'horizontal'; } return 'axisIndex'; } }, /** * @private */ _autoSetAxisIndex: function () { var autoAxisIndex = true; var orient = this.get('orient', true); var thisOption = this.option; var dependentModels = this.dependentModels; if (autoAxisIndex) { // Find axis that parallel to dataZoom as default. var dimName = orient === 'vertical' ? 'y' : 'x'; if (dependentModels[dimName + 'Axis'].length) { thisOption[dimName + 'AxisIndex'] = [0]; autoAxisIndex = false; } else { each$22(dependentModels.singleAxis, function (singleAxisModel) { if (autoAxisIndex && singleAxisModel.get('orient', true) === orient) { thisOption.singleAxisIndex = [singleAxisModel.componentIndex]; autoAxisIndex = false; } }); } } if (autoAxisIndex) { // Find the first category axis as default. (consider polar) eachAxisDim(function (dimNames) { if (!autoAxisIndex) { return; } var axisIndices = []; var axisModels = this.dependentModels[dimNames.axis]; if (axisModels.length && !axisIndices.length) { for (var i = 0, len = axisModels.length; i < len; i++) { if (axisModels[i].get('type') === 'category') { axisIndices.push(i); } } } thisOption[dimNames.axisIndex] = axisIndices; if (axisIndices.length) { autoAxisIndex = false; } }, this); } if (autoAxisIndex) { // FIXME // 这里是兼容ec2的写法(没指定xAxisIndex和yAxisIndex时把scatter和双数值轴折柱纳入dataZoom控制), // 但是实际是否需要Grid.js#getScaleByOption来判断(考虑time,log等axis type)? // If both dataZoom.xAxisIndex and dataZoom.yAxisIndex is not specified, // dataZoom component auto adopts series that reference to // both xAxis and yAxis which type is 'value'. this.ecModel.eachSeries(function (seriesModel) { if (this._isSeriesHasAllAxesTypeOf(seriesModel, 'value')) { eachAxisDim(function (dimNames) { var axisIndices = thisOption[dimNames.axisIndex]; var axisIndex = seriesModel.get(dimNames.axisIndex); var axisId = seriesModel.get(dimNames.axisId); var axisModel = seriesModel.ecModel.queryComponents({ mainType: dimNames.axis, index: axisIndex, id: axisId })[0]; if (__DEV__) { if (!axisModel) { throw new Error( dimNames.axis + ' "' + retrieve( axisIndex, axisId, 0 ) + '" not found' ); } } axisIndex = axisModel.componentIndex; if (indexOf(axisIndices, axisIndex) < 0) { axisIndices.push(axisIndex); } }); } }, this); } }, /** * @private */ _autoSetOrient: function () { var dim; // Find the first axis this.eachTargetAxis(function (dimNames) { !dim && (dim = dimNames.name); }, this); this.option.orient = dim === 'y' ? 'vertical' : 'horizontal'; }, /** * @private */ _isSeriesHasAllAxesTypeOf: function (seriesModel, axisType) { // FIXME // 需要series的xAxisIndex和yAxisIndex都首先自动设置上。 // 例如series.type === scatter时。 var is = true; eachAxisDim(function (dimNames) { var seriesAxisIndex = seriesModel.get(dimNames.axisIndex); var axisModel = this.dependentModels[dimNames.axis][seriesAxisIndex]; if (!axisModel || axisModel.get('type') !== axisType) { is = false; } }, this); return is; }, /** * @private */ _setDefaultThrottle: function (rawOption) { // When first time user set throttle, auto throttle ends. if (rawOption.hasOwnProperty('throttle')) { this._autoThrottle = false; } if (this._autoThrottle) { var globalOption = this.ecModel.option; this.option.throttle = (globalOption.animation && globalOption.animationDurationUpdate > 0) ? 100 : 20; } }, /** * @public */ getFirstTargetAxisModel: function () { var firstAxisModel; eachAxisDim(function (dimNames) { if (firstAxisModel == null) { var indices = this.get(dimNames.axisIndex); if (indices.length) { firstAxisModel = this.dependentModels[dimNames.axis][indices[0]]; } } }, this); return firstAxisModel; }, /** * @public * @param {Function} callback param: axisModel, dimNames, axisIndex, dataZoomModel, ecModel */ eachTargetAxis: function (callback, context) { var ecModel = this.ecModel; eachAxisDim(function (dimNames) { each$22( this.get(dimNames.axisIndex), function (axisIndex) { callback.call(context, dimNames, axisIndex, this, ecModel); }, this ); }, this); }, /** * @param {string} dimName * @param {number} axisIndex * @return {module:echarts/component/dataZoom/AxisProxy} If not found, return null/undefined. */ getAxisProxy: function (dimName, axisIndex) { return this._axisProxies[dimName + '_' + axisIndex]; }, /** * @param {string} dimName * @param {number} axisIndex * @return {module:echarts/model/Model} If not found, return null/undefined. */ getAxisModel: function (dimName, axisIndex) { var axisProxy = this.getAxisProxy(dimName, axisIndex); return axisProxy && axisProxy.getAxisModel(); }, /** * If not specified, set to undefined. * * @public * @param {Object} opt * @param {number} [opt.start] * @param {number} [opt.end] * @param {number} [opt.startValue] * @param {number} [opt.endValue] * @param {boolean} [ignoreUpdateRangeUsg=false] */ setRawRange: function (opt, ignoreUpdateRangeUsg) { var option = this.option; each$22([['start', 'startValue'], ['end', 'endValue']], function (names) { // If only one of 'start' and 'startValue' is not null/undefined, the other // should be cleared, which enable clear the option. // If both of them are not set, keep option with the original value, which // enable use only set start but not set end when calling `dispatchAction`. // The same as 'end' and 'endValue'. if (opt[names[0]] != null || opt[names[1]] != null) { option[names[0]] = opt[names[0]]; option[names[1]] = opt[names[1]]; } }, this); !ignoreUpdateRangeUsg && updateRangeUse(this, opt); }, /** * @public * @return {Array.<number>} [startPercent, endPercent] */ getPercentRange: function () { var axisProxy = this.findRepresentativeAxisProxy(); if (axisProxy) { return axisProxy.getDataPercentWindow(); } }, /** * @public * For example, chart.getModel().getComponent('dataZoom').getValueRange('y', 0); * * @param {string} [axisDimName] * @param {number} [axisIndex] * @return {Array.<number>} [startValue, endValue] value can only be '-' or finite number. */ getValueRange: function (axisDimName, axisIndex) { if (axisDimName == null && axisIndex == null) { var axisProxy = this.findRepresentativeAxisProxy(); if (axisProxy) { return axisProxy.getDataValueWindow(); } } else { return this.getAxisProxy(axisDimName, axisIndex).getDataValueWindow(); } }, /** * @public * @param {module:echarts/model/Model} [axisModel] If axisModel given, find axisProxy * corresponding to the axisModel * @return {module:echarts/component/dataZoom/AxisProxy} */ findRepresentativeAxisProxy: function (axisModel) { if (axisModel) { return axisModel.__dzAxisProxy; } // Find the first hosted axisProxy var axisProxies = this._axisProxies; for (var key in axisProxies) { if (axisProxies.hasOwnProperty(key) && axisProxies[key].hostedBy(this)) { return axisProxies[key]; } } // If no hosted axis find not hosted axisProxy. // Consider this case: dataZoomModel1 and dataZoomModel2 control the same axis, // and the option.start or option.end settings are different. The percentRange // should follow axisProxy. // (We encounter this problem in toolbox data zoom.) for (var key in axisProxies) { if (axisProxies.hasOwnProperty(key) && !axisProxies[key].hostedBy(this)) { return axisProxies[key]; } } }, /** * @return {Array.<string>} */ getRangePropMode: function () { return this._rangePropMode.slice(); } }); function retrieveRaw(option) { var ret = {}; each$22( ['start', 'end', 'startValue', 'endValue', 'throttle'], function (name) { option.hasOwnProperty(name) && (ret[name] = option[name]); } ); return ret; } function updateRangeUse(dataZoomModel, rawOption) { var rangePropMode = dataZoomModel._rangePropMode; var rangeModeInOption = dataZoomModel.get('rangeMode'); each$22([['start', 'startValue'], ['end', 'endValue']], function (names, index) { var percentSpecified = rawOption[names[0]] != null; var valueSpecified = rawOption[names[1]] != null; if (percentSpecified && !valueSpecified) { rangePropMode[index] = 'percent'; } else if (!percentSpecified && valueSpecified) { rangePropMode[index] = 'value'; } else if (rangeModeInOption) { rangePropMode[index] = rangeModeInOption[index]; } else if (percentSpecified) { // percentSpecified && valueSpecified rangePropMode[index] = 'percent'; } // else remain its original setting. }); } var DataZoomView = Component.extend({ type: 'dataZoom', render: function (dataZoomModel, ecModel, api, payload) { this.dataZoomModel = dataZoomModel; this.ecModel = ecModel; this.api = api; }, /** * Find the first target coordinate system. * * @protected * @return {Object} { * grid: [ * {model: coord0, axisModels: [axis1, axis3], coordIndex: 1}, * {model: coord1, axisModels: [axis0, axis2], coordIndex: 0}, * ... * ], // cartesians must not be null/undefined. * polar: [ * {model: coord0, axisModels: [axis4], coordIndex: 0}, * ... * ], // polars must not be null/undefined. * singleAxis: [ * {model: coord0, axisModels: [], coordIndex: 0} * ] */ getTargetCoordInfo: function () { var dataZoomModel = this.dataZoomModel; var ecModel = this.ecModel; var coordSysLists = {}; dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) { var axisModel = ecModel.getComponent(dimNames.axis, axisIndex); if (axisModel) { var coordModel = axisModel.getCoordSysModel(); coordModel && save( coordModel, axisModel, coordSysLists[coordModel.mainType] || (coordSysLists[coordModel.mainType] = []), coordModel.componentIndex ); } }, this); function save(coordModel, axisModel, store, coordIndex) { var item; for (var i = 0; i < store.length; i++) { if (store[i].model === coordModel) { item = store[i]; break; } } if (!item) { store.push(item = { model: coordModel, axisModels: [], coordIndex: coordIndex }); } item.axisModels.push(axisModel); } return coordSysLists; } }); var SliderZoomModel = DataZoomModel.extend({ type: 'dataZoom.slider', layoutMode: 'box', /** * @protected */ defaultOption: { show: true, // ph => placeholder. Using placehoder here because // deault value can only be drived in view stage. right: 'ph', // Default align to grid rect. top: 'ph', // Default align to grid rect. width: 'ph', // Default align to grid rect. height: 'ph', // Default align to grid rect. left: null, // Default align to grid rect. bottom: null, // Default align to grid rect. backgroundColor: 'rgba(47,69,84,0)', // Background of slider zoom component. // dataBackgroundColor: '#ddd', // Background coor of data shadow and border of box, // highest priority, remain for compatibility of // previous version, but not recommended any more. dataBackground: { lineStyle: { color: '#2f4554', width: 0.5, opacity: 0.3 }, areaStyle: { color: 'rgba(47,69,84,0.3)', opacity: 0.3 } }, borderColor: '#ddd', // border color of the box. For compatibility, // if dataBackgroundColor is set, borderColor // is ignored. fillerColor: 'rgba(167,183,204,0.4)', // Color of selected area. // handleColor: 'rgba(89,170,216,0.95)', // Color of handle. // handleIcon: 'path://M4.9,17.8c0-1.4,4.5-10.5,5.5-12.4c0-0.1,0.6-1.1,0.9-1.1c0.4,0,0.9,1,0.9,1.1c1.1,2.2,5.4,11,5.4,12.4v17.8c0,1.5-0.6,2.1-1.3,2.1H6.1c-0.7,0-1.3-0.6-1.3-2.1V17.8z', handleIcon: 'M8.2,13.6V3.9H6.3v9.7H3.1v14.9h3.3v9.7h1.8v-9.7h3.3V13.6H8.2z M9.7,24.4H4.8v-1.4h4.9V24.4z M9.7,19.1H4.8v-1.4h4.9V19.1z', // Percent of the slider height handleSize: '100%', handleStyle: { color: '#a7b7cc' }, labelPrecision: null, labelFormatter: null, showDetail: true, showDataShadow: 'auto', // Default auto decision. realtime: true, zoomLock: false, // Whether disable zoom. textStyle: { color: '#333' } } }); var Rect$2 = Rect; var linearMap$2 = linearMap; var asc$2 = asc; var bind$4 = bind; var each$24 = each$1; // Constants var DEFAULT_LOCATION_EDGE_GAP = 7; var DEFAULT_FRAME_BORDER_WIDTH = 1; var DEFAULT_FILLER_SIZE = 30; var HORIZONTAL = 'horizontal'; var VERTICAL = 'vertical'; var LABEL_GAP = 5; var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter']; var SliderZoomView = DataZoomView.extend({ type: 'dataZoom.slider', init: function (ecModel, api) { /** * @private * @type {Object} */ this._displayables = {}; /** * @private * @type {string} */ this._orient; /** * [0, 100] * @private */ this._range; /** * [coord of the first handle, coord of the second handle] * @private */ this._handleEnds; /** * [length, thick] * @private * @type {Array.<number>} */ this._size; /** * @private * @type {number} */ this._handleWidth; /** * @private * @type {number} */ this._handleHeight; /** * @private */ this._location; /** * @private */ this._dragging; /** * @private */ this._dataShadowInfo; this.api = api; }, /** * @override */ render: function (dataZoomModel, ecModel, api, payload) { SliderZoomView.superApply(this, 'render', arguments); createOrUpdate( this, '_dispatchZoomAction', this.dataZoomModel.get('throttle'), 'fixRate' ); this._orient = dataZoomModel.get('orient'); if (this.dataZoomModel.get('show') === false) { this.group.removeAll(); return; } // Notice: this._resetInterval() should not be executed when payload.type // is 'dataZoom', origin this._range should be maintained, otherwise 'pan' // or 'zoom' info will be missed because of 'throttle' of this.dispatchAction, if (!payload || payload.type !== 'dataZoom' || payload.from !== this.uid) { this._buildView(); } this._updateView(); }, /** * @override */ remove: function () { SliderZoomView.superApply(this, 'remove', arguments); clear(this, '_dispatchZoomAction'); }, /** * @override */ dispose: function () { SliderZoomView.superApply(this, 'dispose', arguments); clear(this, '_dispatchZoomAction'); }, _buildView: function () { var thisGroup = this.group; thisGroup.removeAll(); this._resetLocation(); this._resetInterval(); var barGroup = this._displayables.barGroup = new Group(); this._renderBackground(); this._renderHandle(); this._renderDataShadow(); thisGroup.add(barGroup); this._positionGroup(); }, /** * @private */ _resetLocation: function () { var dataZoomModel = this.dataZoomModel; var api = this.api; // If some of x/y/width/height are not specified, // auto-adapt according to target grid. var coordRect = this._findCoordRect(); var ecSize = {width: api.getWidth(), height: api.getHeight()}; // Default align by coordinate system rect. var positionInfo = this._orient === HORIZONTAL ? { // Why using 'right', because right should be used in vertical, // and it is better to be consistent for dealing with position param merge. right: ecSize.width - coordRect.x - coordRect.width, top: (ecSize.height - DEFAULT_FILLER_SIZE - DEFAULT_LOCATION_EDGE_GAP), width: coordRect.width, height: DEFAULT_FILLER_SIZE } : { // vertical right: DEFAULT_LOCATION_EDGE_GAP, top: coordRect.y, width: DEFAULT_FILLER_SIZE, height: coordRect.height }; // Do not write back to option and replace value 'ph', because // the 'ph' value should be recalculated when resize. var layoutParams = getLayoutParams(dataZoomModel.option); // Replace the placeholder value. each$1(['right', 'top', 'width', 'height'], function (name) { if (layoutParams[name] === 'ph') { layoutParams[name] = positionInfo[name]; } }); var layoutRect = getLayoutRect( layoutParams, ecSize, dataZoomModel.padding ); this._location = {x: layoutRect.x, y: layoutRect.y}; this._size = [layoutRect.width, layoutRect.height]; this._orient === VERTICAL && this._size.reverse(); }, /** * @private */ _positionGroup: function () { var thisGroup = this.group; var location = this._location; var orient = this._orient; // Just use the first axis to determine mapping. var targetAxisModel = this.dataZoomModel.getFirstTargetAxisModel(); var inverse = targetAxisModel && targetAxisModel.get('inverse'); var barGroup = this._displayables.barGroup; var otherAxisInverse = (this._dataShadowInfo || {}).otherAxisInverse; // Transform barGroup. barGroup.attr( (orient === HORIZONTAL && !inverse) ? {scale: otherAxisInverse ? [1, 1] : [1, -1]} : (orient === HORIZONTAL && inverse) ? {scale: otherAxisInverse ? [-1, 1] : [-1, -1]} : (orient === VERTICAL && !inverse) ? {scale: otherAxisInverse ? [1, -1] : [1, 1], rotation: Math.PI / 2} // Dont use Math.PI, considering shadow direction. : {scale: otherAxisInverse ? [-1, -1] : [-1, 1], rotation: Math.PI / 2} ); // Position barGroup var rect = thisGroup.getBoundingRect([barGroup]); thisGroup.attr('position', [location.x - rect.x, location.y - rect.y]); }, /** * @private */ _getViewExtent: function () { return [0, this._size[0]]; }, _renderBackground: function () { var dataZoomModel = this.dataZoomModel; var size = this._size; var barGroup = this._displayables.barGroup; barGroup.add(new Rect$2({ silent: true, shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: dataZoomModel.get('backgroundColor') }, z2: -40 })); // Click panel, over shadow, below handles. barGroup.add(new Rect$2({ shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: 'transparent' }, z2: 0, onclick: bind(this._onClickPanelClick, this) })); }, _renderDataShadow: function () { var info = this._dataShadowInfo = this._prepareDataShadowInfo(); if (!info) { return; } var size = this._size; var seriesModel = info.series; var data = seriesModel.getRawData(); var otherDim = seriesModel.getShadowDim ? seriesModel.getShadowDim() // @see candlestick : info.otherDim; if (otherDim == null) { return; } var otherDataExtent = data.getDataExtent(otherDim); // Nice extent. var otherOffset = (otherDataExtent[1] - otherDataExtent[0]) * 0.3; otherDataExtent = [ otherDataExtent[0] - otherOffset, otherDataExtent[1] + otherOffset ]; var otherShadowExtent = [0, size[1]]; var thisShadowExtent = [0, size[0]]; var areaPoints = [[size[0], 0], [0, 0]]; var linePoints = []; var step = thisShadowExtent[1] / (data.count() - 1); var thisCoord = 0; // Optimize for large data shadow var stride = Math.round(data.count() / size[0]); var lastIsEmpty; data.each([otherDim], function (value, index) { if (stride > 0 && (index % stride)) { thisCoord += step; return; } // FIXME // Should consider axis.min/axis.max when drawing dataShadow. // FIXME // 应该使用统一的空判断?还是在list里进行空判断? var isEmpty = value == null || isNaN(value) || value === ''; // See #4235. var otherCoord = isEmpty ? 0 : linearMap$2(value, otherDataExtent, otherShadowExtent, true); // Attempt to draw data shadow precisely when there are empty value. if (isEmpty && !lastIsEmpty && index) { areaPoints.push([areaPoints[areaPoints.length - 1][0], 0]); linePoints.push([linePoints[linePoints.length - 1][0], 0]); } else if (!isEmpty && lastIsEmpty) { areaPoints.push([thisCoord, 0]); linePoints.push([thisCoord, 0]); } areaPoints.push([thisCoord, otherCoord]); linePoints.push([thisCoord, otherCoord]); thisCoord += step; lastIsEmpty = isEmpty; }); var dataZoomModel = this.dataZoomModel; // var dataBackgroundModel = dataZoomModel.getModel('dataBackground'); this._displayables.barGroup.add(new Polygon({ shape: {points: areaPoints}, style: defaults( {fill: dataZoomModel.get('dataBackgroundColor')}, dataZoomModel.getModel('dataBackground.areaStyle').getAreaStyle() ), silent: true, z2: -20 })); this._displayables.barGroup.add(new Polyline({ shape: {points: linePoints}, style: dataZoomModel.getModel('dataBackground.lineStyle').getLineStyle(), silent: true, z2: -19 })); }, _prepareDataShadowInfo: function () { var dataZoomModel = this.dataZoomModel; var showDataShadow = dataZoomModel.get('showDataShadow'); if (showDataShadow === false) { return; } // Find a representative series. var result; var ecModel = this.ecModel; dataZoomModel.eachTargetAxis(function (dimNames, axisIndex) { var seriesModels = dataZoomModel .getAxisProxy(dimNames.name, axisIndex) .getTargetSeriesModels(); each$1(seriesModels, function (seriesModel) { if (result) { return; } if (showDataShadow !== true && indexOf( SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type') ) < 0 ) { return; } var thisAxis = ecModel.getComponent(dimNames.axis, axisIndex).axis; var otherDim = getOtherDim(dimNames.name); var otherAxisInverse; var coordSys = seriesModel.coordinateSystem; if (otherDim != null && coordSys.getOtherAxis) { otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse; } otherDim = seriesModel.getData().mapDimension(otherDim); result = { thisAxis: thisAxis, series: seriesModel, thisDim: dimNames.name, otherDim: otherDim, otherAxisInverse: otherAxisInverse }; }, this); }, this); return result; }, _renderHandle: function () { var displaybles = this._displayables; var handles = displaybles.handles = []; var handleLabels = displaybles.handleLabels = []; var barGroup = this._displayables.barGroup; var size = this._size; var dataZoomModel = this.dataZoomModel; barGroup.add(displaybles.filler = new Rect$2({ draggable: true, cursor: getCursor(this._orient), drift: bind$4(this._onDragMove, this, 'all'), onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, ondragstart: bind$4(this._showDataInfo, this, true), ondragend: bind$4(this._onDragEnd, this), onmouseover: bind$4(this._showDataInfo, this, true), onmouseout: bind$4(this._showDataInfo, this, false), style: { fill: dataZoomModel.get('fillerColor'), textPosition : 'inside' } })); // Frame border. barGroup.add(new Rect$2(subPixelOptimizeRect({ silent: true, shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { stroke: dataZoomModel.get('dataBackgroundColor') || dataZoomModel.get('borderColor'), lineWidth: DEFAULT_FRAME_BORDER_WIDTH, fill: 'rgba(0,0,0,0)' } }))); each$24([0, 1], function (handleIndex) { var path = createIcon( dataZoomModel.get('handleIcon'), { cursor: getCursor(this._orient), draggable: true, drift: bind$4(this._onDragMove, this, handleIndex), onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, ondragend: bind$4(this._onDragEnd, this), onmouseover: bind$4(this._showDataInfo, this, true), onmouseout: bind$4(this._showDataInfo, this, false) }, {x: -1, y: 0, width: 2, height: 2} ); var bRect = path.getBoundingRect(); this._handleHeight = parsePercent$1(dataZoomModel.get('handleSize'), this._size[1]); this._handleWidth = bRect.width / bRect.height * this._handleHeight; path.setStyle(dataZoomModel.getModel('handleStyle').getItemStyle()); var handleColor = dataZoomModel.get('handleColor'); // Compatitable with previous version if (handleColor != null) { path.style.fill = handleColor; } barGroup.add(handles[handleIndex] = path); var textStyleModel = dataZoomModel.textStyleModel; this.group.add( handleLabels[handleIndex] = new Text({ silent: true, invisible: true, style: { x: 0, y: 0, text: '', textVerticalAlign: 'middle', textAlign: 'center', textFill: textStyleModel.getTextColor(), textFont: textStyleModel.getFont() }, z2: 10 })); }, this); }, /** * @private */ _resetInterval: function () { var range = this._range = this.dataZoomModel.getPercentRange(); var viewExtent = this._getViewExtent(); this._handleEnds = [ linearMap$2(range[0], [0, 100], viewExtent, true), linearMap$2(range[1], [0, 100], viewExtent, true) ]; }, /** * @private * @param {(number|string)} handleIndex 0 or 1 or 'all' * @param {number} delta */ _updateInterval: function (handleIndex, delta) { var dataZoomModel = this.dataZoomModel; var handleEnds = this._handleEnds; var viewExtend = this._getViewExtent(); var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); var percentExtent = [0, 100]; sliderMove( delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap$2(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap$2(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null ); this._range = asc$2([ linearMap$2(handleEnds[0], viewExtend, percentExtent, true), linearMap$2(handleEnds[1], viewExtend, percentExtent, true) ]); }, /** * @private */ _updateView: function (nonRealtime) { var displaybles = this._displayables; var handleEnds = this._handleEnds; var handleInterval = asc$2(handleEnds.slice()); var size = this._size; each$24([0, 1], function (handleIndex) { // Handles var handle = displaybles.handles[handleIndex]; var handleHeight = this._handleHeight; handle.attr({ scale: [handleHeight / 2, handleHeight / 2], position: [handleEnds[handleIndex], size[1] / 2 - handleHeight / 2] }); }, this); // Filler displaybles.filler.setShape({ x: handleInterval[0], y: 0, width: handleInterval[1] - handleInterval[0], height: size[1] }); this._updateDataInfo(nonRealtime); }, /** * @private */ _updateDataInfo: function (nonRealtime) { var dataZoomModel = this.dataZoomModel; var displaybles = this._displayables; var handleLabels = displaybles.handleLabels; var orient = this._orient; var labelTexts = ['', '']; // FIXME // date型,支持formatter,autoformatter(ec2 date.getAutoFormatter) if (dataZoomModel.get('showDetail')) { var axisProxy = dataZoomModel.findRepresentativeAxisProxy(); if (axisProxy) { var axis = axisProxy.getAxisModel().axis; var range = this._range; var dataInterval = nonRealtime // See #4434, data and axis are not processed and reset yet in non-realtime mode. ? axisProxy.calculateDataWindow({ start: range[0], end: range[1] }).valueWindow : axisProxy.getDataValueWindow(); labelTexts = [ this._formatLabel(dataInterval[0], axis), this._formatLabel(dataInterval[1], axis) ]; } } var orderedHandleEnds = asc$2(this._handleEnds.slice()); setLabel.call(this, 0); setLabel.call(this, 1); function setLabel(handleIndex) { // Label // Text should not transform by barGroup. // Ignore handlers transform var barTransform = getTransform( displaybles.handles[handleIndex].parent, this.group ); var direction = transformDirection( handleIndex === 0 ? 'right' : 'left', barTransform ); var offset = this._handleWidth / 2 + LABEL_GAP; var textPoint = applyTransform$1( [ orderedHandleEnds[handleIndex] + (handleIndex === 0 ? -offset : offset), this._size[1] / 2 ], barTransform ); handleLabels[handleIndex].setStyle({ x: textPoint[0], y: textPoint[1], textVerticalAlign: orient === HORIZONTAL ? 'middle' : direction, textAlign: orient === HORIZONTAL ? direction : 'center', text: labelTexts[handleIndex] }); } }, /** * @private */ _formatLabel: function (value, axis) { var dataZoomModel = this.dataZoomModel; var labelFormatter = dataZoomModel.get('labelFormatter'); var labelPrecision = dataZoomModel.get('labelPrecision'); if (labelPrecision == null || labelPrecision === 'auto') { labelPrecision = axis.getPixelPrecision(); } var valueStr = (value == null || isNaN(value)) ? '' // FIXME Glue code : (axis.type === 'category' || axis.type === 'time') ? axis.scale.getLabel(Math.round(value)) // param of toFixed should less then 20. : value.toFixed(Math.min(labelPrecision, 20)); return isFunction$1(labelFormatter) ? labelFormatter(value, valueStr) : isString(labelFormatter) ? labelFormatter.replace('{value}', valueStr) : valueStr; }, /** * @private * @param {boolean} showOrHide true: show, false: hide */ _showDataInfo: function (showOrHide) { // Always show when drgging. showOrHide = this._dragging || showOrHide; var handleLabels = this._displayables.handleLabels; handleLabels[0].attr('invisible', !showOrHide); handleLabels[1].attr('invisible', !showOrHide); }, _onDragMove: function (handleIndex, dx, dy) { this._dragging = true; // Transform dx, dy to bar coordination. var barTransform = this._displayables.barGroup.getLocalTransform(); var vertex = applyTransform$1([dx, dy], barTransform, true); this._updateInterval(handleIndex, vertex[0]); var realtime = this.dataZoomModel.get('realtime'); this._updateView(!realtime); realtime && this._dispatchZoomAction(); }, _onDragEnd: function () { this._dragging = false; this._showDataInfo(false); // While in realtime mode and stream mode, dispatch action when // drag end will cause the whole view rerender, which is unnecessary. var realtime = this.dataZoomModel.get('realtime'); !realtime && this._dispatchZoomAction(); }, _onClickPanelClick: function (e) { var size = this._size; var localPoint = this._displayables.barGroup.transformCoordToLocal(e.offsetX, e.offsetY); if (localPoint[0] < 0 || localPoint[0] > size[0] || localPoint[1] < 0 || localPoint[1] > size[1] ) { return; } var handleEnds = this._handleEnds; var center = (handleEnds[0] + handleEnds[1]) / 2; this._updateInterval('all', localPoint[0] - center); this._updateView(); this._dispatchZoomAction(); }, /** * This action will be throttled. * @private */ _dispatchZoomAction: function () { var range = this._range; this.api.dispatchAction({ type: 'dataZoom', from: this.uid, dataZoomId: this.dataZoomModel.id, start: range[0], end: range[1] }); }, /** * @private */ _findCoordRect: function () { // Find the grid coresponding to the first axis referred by dataZoom. var rect; each$24(this.getTargetCoordInfo(), function (coordInfoList) { if (!rect && coordInfoList.length) { var coordSys = coordInfoList[0].model.coordinateSystem; rect = coordSys.getRect && coordSys.getRect(); } }); if (!rect) { var width = this.api.getWidth(); var height = this.api.getHeight(); rect = { x: width * 0.2, y: height * 0.2, width: width * 0.6, height: height * 0.6 }; } return rect; } }); function getOtherDim(thisDim) { // FIXME // 这个逻辑和getOtherAxis里一致,但是写在这里是否不好 var map$$1 = {x: 'y', y: 'x', radius: 'angle', angle: 'radius'}; return map$$1[thisDim]; } function getCursor(orient) { return orient === 'vertical' ? 'ns-resize' : 'ew-resize'; } DataZoomModel.extend({ type: 'dataZoom.inside', /** * @protected */ defaultOption: { disabled: false, // Whether disable this inside zoom. zoomLock: false, // Whether disable zoom but only pan. zoomOnMouseWheel: true, // Can be: true / false / 'shift' / 'ctrl' / 'alt'. moveOnMouseMove: true, // Can be: true / false / 'shift' / 'ctrl' / 'alt'. preventDefaultMouseMove: true } }); // Only create one roam controller for each coordinate system. // one roam controller might be refered by two inside data zoom // components (for example, one for x and one for y). When user // pan or zoom, only dispatch one action for those data zoom // components. var curry$6 = curry; var ATTR$1 = '\0_ec_dataZoom_roams'; /** * @public * @param {module:echarts/ExtensionAPI} api * @param {Object} dataZoomInfo * @param {string} dataZoomInfo.coordId * @param {Function} dataZoomInfo.containsPoint * @param {Array.<string>} dataZoomInfo.allCoordIds * @param {string} dataZoomInfo.dataZoomId * @param {number} dataZoomInfo.throttleRate * @param {Function} dataZoomInfo.panGetRange * @param {Function} dataZoomInfo.zoomGetRange * @param {boolean} [dataZoomInfo.zoomLock] * @param {boolean} [dataZoomInfo.disabled] */ function register$2(api, dataZoomInfo) { var store = giveStore(api); var theDataZoomId = dataZoomInfo.dataZoomId; var theCoordId = dataZoomInfo.coordId; // Do clean when a dataZoom changes its target coordnate system. // Avoid memory leak, dispose all not-used-registered. each$1(store, function (record, coordId) { var dataZoomInfos = record.dataZoomInfos; if (dataZoomInfos[theDataZoomId] && indexOf(dataZoomInfo.allCoordIds, theCoordId) < 0 ) { delete dataZoomInfos[theDataZoomId]; record.count--; } }); cleanStore(store); var record = store[theCoordId]; // Create if needed. if (!record) { record = store[theCoordId] = { coordId: theCoordId, dataZoomInfos: {}, count: 0 }; record.controller = createController(api, record); record.dispatchAction = curry(dispatchAction$1, api); } // Update reference of dataZoom. !(record.dataZoomInfos[theDataZoomId]) && record.count++; record.dataZoomInfos[theDataZoomId] = dataZoomInfo; var controllerParams = mergeControllerParams(record.dataZoomInfos); record.controller.enable(controllerParams.controlType, controllerParams.opt); // Consider resize, area should be always updated. record.controller.setPointerChecker(dataZoomInfo.containsPoint); // Update throttle. createOrUpdate( record, 'dispatchAction', dataZoomInfo.throttleRate, 'fixRate' ); } /** * @public * @param {module:echarts/ExtensionAPI} api * @param {string} dataZoomId */ function unregister$1(api, dataZoomId) { var store = giveStore(api); each$1(store, function (record) { record.controller.dispose(); var dataZoomInfos = record.dataZoomInfos; if (dataZoomInfos[dataZoomId]) { delete dataZoomInfos[dataZoomId]; record.count--; } }); cleanStore(store); } /** * @public */ function shouldRecordRange(payload, dataZoomId) { if (payload && payload.type === 'dataZoom' && payload.batch) { for (var i = 0, len = payload.batch.length; i < len; i++) { if (payload.batch[i].dataZoomId === dataZoomId) { return false; } } } return true; } /** * @public */ function generateCoordId(coordModel) { return coordModel.type + '\0_' + coordModel.id; } /** * Key: coordId, value: {dataZoomInfos: [], count, controller} * @type {Array.<Object>} */ function giveStore(api) { // Mount store on zrender instance, so that we do not // need to worry about dispose. var zr = api.getZr(); return zr[ATTR$1] || (zr[ATTR$1] = {}); } function createController(api, newRecord) { var controller = new RoamController(api.getZr()); controller.on('pan', curry$6(onPan, newRecord)); controller.on('zoom', curry$6(onZoom, newRecord)); return controller; } function cleanStore(store) { each$1(store, function (record, coordId) { if (!record.count) { record.controller.dispose(); delete store[coordId]; } }); } function onPan(record, dx, dy, oldX, oldY, newX, newY) { wrapAndDispatch(record, function (info) { return info.panGetRange(record.controller, dx, dy, oldX, oldY, newX, newY); }); } function onZoom(record, scale, mouseX, mouseY) { wrapAndDispatch(record, function (info) { return info.zoomGetRange(record.controller, scale, mouseX, mouseY); }); } function wrapAndDispatch(record, getRange) { var batch = []; each$1(record.dataZoomInfos, function (info) { var range = getRange(info); !info.disabled && range && batch.push({ dataZoomId: info.dataZoomId, start: range[0], end: range[1] }); }); record.dispatchAction(batch); } /** * This action will be throttled. */ function dispatchAction$1(api, batch) { api.dispatchAction({ type: 'dataZoom', batch: batch }); } /** * Merge roamController settings when multiple dataZooms share one roamController. */ function mergeControllerParams(dataZoomInfos) { var controlType; var opt = {}; // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated // as string, it is probably revert to reserved word by compress tool. See #7411. var prefix = 'type_'; var typePriority = { 'type_true': 2, 'type_move': 1, 'type_false': 0, 'type_undefined': -1 }; each$1(dataZoomInfos, function (dataZoomInfo) { var oneType = dataZoomInfo.disabled ? false : dataZoomInfo.zoomLock ? 'move' : true; if (typePriority[prefix + oneType] > typePriority[prefix + controlType]) { controlType = oneType; } // Do not support that different 'shift'/'ctrl'/'alt' setting used in one coord sys. extend(opt, dataZoomInfo.roamControllerOpt); }); return { controlType: controlType, opt: opt }; } var bind$5 = bind; var InsideZoomView = DataZoomView.extend({ type: 'dataZoom.inside', /** * @override */ init: function (ecModel, api) { /** * 'throttle' is used in this.dispatchAction, so we save range * to avoid missing some 'pan' info. * @private * @type {Array.<number>} */ this._range; }, /** * @override */ render: function (dataZoomModel, ecModel, api, payload) { InsideZoomView.superApply(this, 'render', arguments); // Notice: origin this._range should be maintained, and should not be re-fetched // from dataZoomModel when payload.type is 'dataZoom', otherwise 'pan' or 'zoom' // info will be missed because of 'throttle' of this.dispatchAction. if (shouldRecordRange(payload, dataZoomModel.id)) { this._range = dataZoomModel.getPercentRange(); } // Reset controllers. each$1(this.getTargetCoordInfo(), function (coordInfoList, coordSysName) { var allCoordIds = map(coordInfoList, function (coordInfo) { return generateCoordId(coordInfo.model); }); each$1(coordInfoList, function (coordInfo) { var coordModel = coordInfo.model; var dataZoomOption = dataZoomModel.option; register$2( api, { coordId: generateCoordId(coordModel), allCoordIds: allCoordIds, containsPoint: function (e, x, y) { return coordModel.coordinateSystem.containPoint([x, y]); }, dataZoomId: dataZoomModel.id, throttleRate: dataZoomModel.get('throttle', true), panGetRange: bind$5(this._onPan, this, coordInfo, coordSysName), zoomGetRange: bind$5(this._onZoom, this, coordInfo, coordSysName), zoomLock: dataZoomOption.zoomLock, disabled: dataZoomOption.disabled, roamControllerOpt: { zoomOnMouseWheel: dataZoomOption.zoomOnMouseWheel, moveOnMouseMove: dataZoomOption.moveOnMouseMove, preventDefaultMouseMove: dataZoomOption.preventDefaultMouseMove } } ); }, this); }, this); }, /** * @override */ dispose: function () { unregister$1(this.api, this.dataZoomModel.id); InsideZoomView.superApply(this, 'dispose', arguments); this._range = null; }, /** * @private */ _onPan: function (coordInfo, coordSysName, controller, dx, dy, oldX, oldY, newX, newY) { var range = this._range.slice(); // Calculate transform by the first axis. var axisModel = coordInfo.axisModels[0]; if (!axisModel) { return; } var directionInfo = getDirectionInfo[coordSysName]( [oldX, oldY], [newX, newY], axisModel, controller, coordInfo ); var percentDelta = directionInfo.signal * (range[1] - range[0]) * directionInfo.pixel / directionInfo.pixelLength; sliderMove(percentDelta, range, [0, 100], 'all'); return (this._range = range); }, /** * @private */ _onZoom: function (coordInfo, coordSysName, controller, scale, mouseX, mouseY) { var range = this._range.slice(); // Calculate transform by the first axis. var axisModel = coordInfo.axisModels[0]; if (!axisModel) { return; } var directionInfo = getDirectionInfo[coordSysName]( null, [mouseX, mouseY], axisModel, controller, coordInfo ); var percentPoint = ( directionInfo.signal > 0 ? (directionInfo.pixelStart + directionInfo.pixelLength - directionInfo.pixel) : (directionInfo.pixel - directionInfo.pixelStart) ) / directionInfo.pixelLength * (range[1] - range[0]) + range[0]; scale = Math.max(1 / scale, 0); range[0] = (range[0] - percentPoint) * scale + percentPoint; range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range. var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan); return (this._range = range); } }); var getDirectionInfo = { grid: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var ret = {}; var rect = coordInfo.model.coordinateSystem.getRect(); oldPoint = oldPoint || [0, 0]; if (axis.dim === 'x') { ret.pixel = newPoint[0] - oldPoint[0]; ret.pixelLength = rect.width; ret.pixelStart = rect.x; ret.signal = axis.inverse ? 1 : -1; } else { // axis.dim === 'y' ret.pixel = newPoint[1] - oldPoint[1]; ret.pixelLength = rect.height; ret.pixelStart = rect.y; ret.signal = axis.inverse ? -1 : 1; } return ret; }, polar: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var ret = {}; var polar = coordInfo.model.coordinateSystem; var radiusExtent = polar.getRadiusAxis().getExtent(); var angleExtent = polar.getAngleAxis().getExtent(); oldPoint = oldPoint ? polar.pointToCoord(oldPoint) : [0, 0]; newPoint = polar.pointToCoord(newPoint); if (axisModel.mainType === 'radiusAxis') { ret.pixel = newPoint[0] - oldPoint[0]; // ret.pixelLength = Math.abs(radiusExtent[1] - radiusExtent[0]); // ret.pixelStart = Math.min(radiusExtent[0], radiusExtent[1]); ret.pixelLength = radiusExtent[1] - radiusExtent[0]; ret.pixelStart = radiusExtent[0]; ret.signal = axis.inverse ? 1 : -1; } else { // 'angleAxis' ret.pixel = newPoint[1] - oldPoint[1]; // ret.pixelLength = Math.abs(angleExtent[1] - angleExtent[0]); // ret.pixelStart = Math.min(angleExtent[0], angleExtent[1]); ret.pixelLength = angleExtent[1] - angleExtent[0]; ret.pixelStart = angleExtent[0]; ret.signal = axis.inverse ? -1 : 1; } return ret; }, singleAxis: function (oldPoint, newPoint, axisModel, controller, coordInfo) { var axis = axisModel.axis; var rect = coordInfo.model.coordinateSystem.getRect(); var ret = {}; oldPoint = oldPoint || [0, 0]; if (axis.orient === 'horizontal') { ret.pixel = newPoint[0] - oldPoint[0]; ret.pixelLength = rect.width; ret.pixelStart = rect.x; ret.signal = axis.inverse ? 1 : -1; } else { // 'vertical' ret.pixel = newPoint[1] - oldPoint[1]; ret.pixelLength = rect.height; ret.pixelStart = rect.y; ret.signal = axis.inverse ? -1 : 1; } return ret; } }; registerProcessor({ getTargetSeries: function (ecModel) { var seriesModelMap = createHashMap(); ecModel.eachComponent('dataZoom', function (dataZoomModel) { dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { var axisProxy = dataZoomModel.getAxisProxy(dimNames.name, axisIndex); each$1(axisProxy.getTargetSeriesModels(), function (seriesModel) { seriesModelMap.set(seriesModel.uid, seriesModel); }); }); }); return seriesModelMap; }, isOverallFilter: true, // Consider appendData, where filter should be performed. Because data process is // in block mode currently, it is not need to worry about that the overallProgress // execute every frame. overallReset: function (ecModel, api) { ecModel.eachComponent('dataZoom', function (dataZoomModel) { // We calculate window and reset axis here but not in model // init stage and not after action dispatch handler, because // reset should be called after seriesData.restoreData. dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { dataZoomModel.getAxisProxy(dimNames.name, axisIndex).reset(dataZoomModel, api); }); // Caution: data zoom filtering is order sensitive when using // percent range and no min/max/scale set on axis. // For example, we have dataZoom definition: // [ // {xAxisIndex: 0, start: 30, end: 70}, // {yAxisIndex: 0, start: 20, end: 80} // ] // In this case, [20, 80] of y-dataZoom should be based on data // that have filtered by x-dataZoom using range of [30, 70], // but should not be based on full raw data. Thus sliding // x-dataZoom will change both ranges of xAxis and yAxis, // while sliding y-dataZoom will only change the range of yAxis. // So we should filter x-axis after reset x-axis immediately, // and then reset y-axis and filter y-axis. dataZoomModel.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel) { dataZoomModel.getAxisProxy(dimNames.name, axisIndex).filterData(dataZoomModel, api); }); }); ecModel.eachComponent('dataZoom', function (dataZoomModel) { // Fullfill all of the range props so that user // is able to get them from chart.getOption(). var axisProxy = dataZoomModel.findRepresentativeAxisProxy(); var percentRange = axisProxy.getDataPercentWindow(); var valueRange = axisProxy.getDataValueWindow(); dataZoomModel.setRawRange({ start: percentRange[0], end: percentRange[1], startValue: valueRange[0], endValue: valueRange[1] }, true); }); } }); registerAction('dataZoom', function (payload, ecModel) { var linkedNodesFinder = createLinkedNodesFinder( bind(ecModel.eachComponent, ecModel, 'dataZoom'), eachAxisDim$1, function (model, dimNames) { return model.get(dimNames.axisIndex); } ); var effectedModels = []; ecModel.eachComponent( {mainType: 'dataZoom', query: payload}, function (model, index) { effectedModels.push.apply( effectedModels, linkedNodesFinder(model).nodes ); } ); each$1(effectedModels, function (dataZoomModel, index) { dataZoomModel.setRawRange({ start: payload.start, end: payload.end, startValue: payload.startValue, endValue: payload.endValue }); }); }); /** * DataZoom component entry */ var each$25 = each$1; var preprocessor$2 = function (option) { var visualMap = option && option.visualMap; if (!isArray(visualMap)) { visualMap = visualMap ? [visualMap] : []; } each$25(visualMap, function (opt) { if (!opt) { return; } // rename splitList to pieces if (has$1(opt, 'splitList') && !has$1(opt, 'pieces')) { opt.pieces = opt.splitList; delete opt.splitList; } var pieces = opt.pieces; if (pieces && isArray(pieces)) { each$25(pieces, function (piece) { if (isObject$1(piece)) { if (has$1(piece, 'start') && !has$1(piece, 'min')) { piece.min = piece.start; } if (has$1(piece, 'end') && !has$1(piece, 'max')) { piece.max = piece.end; } } }); } }); }; function has$1(obj, name) { return obj && obj.hasOwnProperty && obj.hasOwnProperty(name); } ComponentModel.registerSubTypeDefaulter('visualMap', function (option) { // Compatible with ec2, when splitNumber === 0, continuous visualMap will be used. return ( !option.categories && ( !( option.pieces ? option.pieces.length > 0 : option.splitNumber > 0 ) || option.calculable ) ) ? 'continuous' : 'piecewise'; }); var VISUAL_PRIORITY = PRIORITY.VISUAL.COMPONENT; registerVisual(VISUAL_PRIORITY, { createOnAllSeries: true, reset: function (seriesModel, ecModel) { var resetDefines = []; ecModel.eachComponent('visualMap', function (visualMapModel) { if (!visualMapModel.isTargetSeries(seriesModel)) { return; } resetDefines.push(incrementalApplyVisual( visualMapModel.stateList, visualMapModel.targetVisuals, bind(visualMapModel.getValueState, visualMapModel), visualMapModel.getDataDimension(seriesModel.getData()) )); }); return resetDefines; } }); // Only support color. registerVisual(VISUAL_PRIORITY, { createOnAllSeries: true, reset: function (seriesModel, ecModel) { var data = seriesModel.getData(); var visualMetaList = []; ecModel.eachComponent('visualMap', function (visualMapModel) { if (visualMapModel.isTargetSeries(seriesModel)) { var visualMeta = visualMapModel.getVisualMeta( bind(getColorVisual, null, seriesModel, visualMapModel) ) || {stops: [], outerColors: []}; var concreteDim = visualMapModel.getDataDimension(data); var dimInfo = data.getDimensionInfo(concreteDim); if (dimInfo != null) { // visualMeta.dimension should be dimension index, but not concrete dimension. visualMeta.dimension = dimInfo.index; visualMetaList.push(visualMeta); } } }); // console.log(JSON.stringify(visualMetaList.map(a => a.stops))); seriesModel.getData().setVisual('visualMeta', visualMetaList); } }); // FIXME // performance and export for heatmap? // value can be Infinity or -Infinity function getColorVisual(seriesModel, visualMapModel, value, valueState) { var mappings = visualMapModel.targetVisuals[valueState]; var visualTypes = VisualMapping.prepareVisualTypes(mappings); var resultVisual = { color: seriesModel.getData().getVisual('color') // default color. }; for (var i = 0, len = visualTypes.length; i < len; i++) { var type = visualTypes[i]; var mapping = mappings[ type === 'opacity' ? '__alphaForOpacity' : type ]; mapping && mapping.applyVisual(value, getVisual, setVisual); } return resultVisual.color; function getVisual(key) { return resultVisual[key]; } function setVisual(key, value) { resultVisual[key] = value; } } /** * @file Visual mapping. */ var visualDefault = { /** * @public */ get: function (visualType, key, isCategory) { var value = clone( (defaultOption$3[visualType] || {})[key] ); return isCategory ? (isArray(value) ? value[value.length - 1] : value) : value; } }; var defaultOption$3 = { color: { active: ['#006edd', '#e0ffff'], inactive: ['rgba(0,0,0,0)'] }, colorHue: { active: [0, 360], inactive: [0, 0] }, colorSaturation: { active: [0.3, 1], inactive: [0, 0] }, colorLightness: { active: [0.9, 0.5], inactive: [0, 0] }, colorAlpha: { active: [0.3, 1], inactive: [0, 0] }, opacity: { active: [0.3, 1], inactive: [0, 0] }, symbol: { active: ['circle', 'roundRect', 'diamond'], inactive: ['none'] }, symbolSize: { active: [10, 50], inactive: [0, 0] } }; var mapVisual$2 = VisualMapping.mapVisual; var eachVisual = VisualMapping.eachVisual; var isArray$3 = isArray; var each$26 = each$1; var asc$3 = asc; var linearMap$3 = linearMap; var noop$2 = noop; var VisualMapModel = extendComponentModel({ type: 'visualMap', dependencies: ['series'], /** * @readOnly * @type {Array.<string>} */ stateList: ['inRange', 'outOfRange'], /** * @readOnly * @type {Array.<string>} */ replacableOptionKeys: [ 'inRange', 'outOfRange', 'target', 'controller', 'color' ], /** * [lowerBound, upperBound] * * @readOnly * @type {Array.<number>} */ dataBound: [-Infinity, Infinity], /** * @readOnly * @type {string|Object} */ layoutMode: {type: 'box', ignoreSize: true}, /** * @protected */ defaultOption: { show: true, zlevel: 0, z: 4, seriesIndex: 'all', // 'all' or null/undefined: all series. // A number or an array of number: the specified series. // set min: 0, max: 200, only for campatible with ec2. // In fact min max should not have default value. min: 0, // min value, must specified if pieces is not specified. max: 200, // max value, must specified if pieces is not specified. dimension: null, inRange: null, // 'color', 'colorHue', 'colorSaturation', 'colorLightness', 'colorAlpha', // 'symbol', 'symbolSize' outOfRange: null, // 'color', 'colorHue', 'colorSaturation', // 'colorLightness', 'colorAlpha', // 'symbol', 'symbolSize' left: 0, // 'center' ¦ 'left' ¦ 'right' ¦ {number} (px) right: null, // The same as left. top: null, // 'top' ¦ 'bottom' ¦ 'center' ¦ {number} (px) bottom: 0, // The same as top. itemWidth: null, itemHeight: null, inverse: false, orient: 'vertical', // 'horizontal' ¦ 'vertical' backgroundColor: 'rgba(0,0,0,0)', borderColor: '#ccc', // 值域边框颜色 contentColor: '#5793f3', inactiveColor: '#aaa', borderWidth: 0, // 值域边框线宽,单位px,默认为0(无边框) padding: 5, // 值域内边距,单位px,默认各方向内边距为5, // 接受数组分别设定上右下左边距,同css textGap: 10, // precision: 0, // 小数精度,默认为0,无小数点 color: null, //颜色(deprecated,兼容ec2,顺序同pieces,不同于inRange/outOfRange) formatter: null, text: null, // 文本,如['高', '低'],兼容ec2,text[0]对应高值,text[1]对应低值 textStyle: { color: '#333' // 值域文字颜色 } }, /** * @protected */ init: function (option, parentModel, ecModel) { /** * @private * @type {Array.<number>} */ this._dataExtent; /** * @readOnly */ this.targetVisuals = {}; /** * @readOnly */ this.controllerVisuals = {}; /** * @readOnly */ this.textStyleModel; /** * [width, height] * @readOnly * @type {Array.<number>} */ this.itemSize; this.mergeDefaultAndTheme(option, ecModel); }, /** * @protected */ optionUpdated: function (newOption, isInit) { var thisOption = this.option; // FIXME // necessary? // Disable realtime view update if canvas is not supported. if (!env$1.canvasSupported) { thisOption.realtime = false; } !isInit && replaceVisualOption( thisOption, newOption, this.replacableOptionKeys ); this.textStyleModel = this.getModel('textStyle'); this.resetItemSize(); this.completeVisualOption(); }, /** * @protected */ resetVisual: function (supplementVisualOption) { var stateList = this.stateList; supplementVisualOption = bind(supplementVisualOption, this); this.controllerVisuals = createVisualMappings( this.option.controller, stateList, supplementVisualOption ); this.targetVisuals = createVisualMappings( this.option.target, stateList, supplementVisualOption ); }, /** * @protected * @return {Array.<number>} An array of series indices. */ getTargetSeriesIndices: function () { var optionSeriesIndex = this.option.seriesIndex; var seriesIndices = []; if (optionSeriesIndex == null || optionSeriesIndex === 'all') { this.ecModel.eachSeries(function (seriesModel, index) { seriesIndices.push(index); }); } else { seriesIndices = normalizeToArray(optionSeriesIndex); } return seriesIndices; }, /** * @public */ eachTargetSeries: function (callback, context) { each$1(this.getTargetSeriesIndices(), function (seriesIndex) { callback.call(context, this.ecModel.getSeriesByIndex(seriesIndex)); }, this); }, /** * @pubilc */ isTargetSeries: function (seriesModel) { var is = false; this.eachTargetSeries(function (model) { model === seriesModel && (is = true); }); return is; }, /** * @example * this.formatValueText(someVal); // format single numeric value to text. * this.formatValueText(someVal, true); // format single category value to text. * this.formatValueText([min, max]); // format numeric min-max to text. * this.formatValueText([this.dataBound[0], max]); // using data lower bound. * this.formatValueText([min, this.dataBound[1]]); // using data upper bound. * * @param {number|Array.<number>} value Real value, or this.dataBound[0 or 1]. * @param {boolean} [isCategory=false] Only available when value is number. * @param {Array.<string>} edgeSymbols Open-close symbol when value is interval. * @return {string} * @protected */ formatValueText: function(value, isCategory, edgeSymbols) { var option = this.option; var precision = option.precision; var dataBound = this.dataBound; var formatter = option.formatter; var isMinMax; var textValue; edgeSymbols = edgeSymbols || ['<', '>']; if (isArray(value)) { value = value.slice(); isMinMax = true; } textValue = isCategory ? value : (isMinMax ? [toFixed(value[0]), toFixed(value[1])] : toFixed(value) ); if (isString(formatter)) { return formatter .replace('{value}', isMinMax ? textValue[0] : textValue) .replace('{value2}', isMinMax ? textValue[1] : textValue); } else if (isFunction$1(formatter)) { return isMinMax ? formatter(value[0], value[1]) : formatter(value); } if (isMinMax) { if (value[0] === dataBound[0]) { return edgeSymbols[0] + ' ' + textValue[1]; } else if (value[1] === dataBound[1]) { return edgeSymbols[1] + ' ' + textValue[0]; } else { return textValue[0] + ' - ' + textValue[1]; } } else { // Format single value (includes category case). return textValue; } function toFixed(val) { return val === dataBound[0] ? 'min' : val === dataBound[1] ? 'max' : (+val).toFixed(Math.min(precision, 20)); } }, /** * @protected */ resetExtent: function () { var thisOption = this.option; // Can not calculate data extent by data here. // Because series and data may be modified in processing stage. // So we do not support the feature "auto min/max". var extent = asc$3([thisOption.min, thisOption.max]); this._dataExtent = extent; }, /** * @public * @param {module:echarts/data/List} list * @return {string} Concrete dimention. If return null/undefined, * no dimension used. */ getDataDimension: function (list) { var optDim = this.option.dimension; var listDimensions = list.dimensions; if (optDim == null && !listDimensions.length) { return; } if (optDim != null) { return list.getDimension(optDim); } var dimNames = list.dimensions; for (var i = dimNames.length - 1; i >= 0; i--) { var dimName = dimNames[i]; var dimInfo = list.getDimensionInfo(dimName); if (!dimInfo.isCalculationCoord) { return dimName; } } }, /** * @public * @override */ getExtent: function () { return this._dataExtent.slice(); }, /** * @protected */ completeVisualOption: function () { var ecModel = this.ecModel; var thisOption = this.option; var base = {inRange: thisOption.inRange, outOfRange: thisOption.outOfRange}; var target = thisOption.target || (thisOption.target = {}); var controller = thisOption.controller || (thisOption.controller = {}); merge(target, base); // Do not override merge(controller, base); // Do not override var isCategory = this.isCategory(); completeSingle.call(this, target); completeSingle.call(this, controller); completeInactive.call(this, target, 'inRange', 'outOfRange'); // completeInactive.call(this, target, 'outOfRange', 'inRange'); completeController.call(this, controller); function completeSingle(base) { // Compatible with ec2 dataRange.color. // The mapping order of dataRange.color is: [high value, ..., low value] // whereas inRange.color and outOfRange.color is [low value, ..., high value] // Notice: ec2 has no inverse. if (isArray$3(thisOption.color) // If there has been inRange: {symbol: ...}, adding color is a mistake. // So adding color only when no inRange defined. && !base.inRange ) { base.inRange = {color: thisOption.color.slice().reverse()}; } // Compatible with previous logic, always give a defautl color, otherwise // simple config with no inRange and outOfRange will not work. // Originally we use visualMap.color as the default color, but setOption at // the second time the default color will be erased. So we change to use // constant DEFAULT_COLOR. // If user do not want the defualt color, set inRange: {color: null}. base.inRange = base.inRange || {color: ecModel.get('gradientColor')}; // If using shortcut like: {inRange: 'symbol'}, complete default value. each$26(this.stateList, function (state) { var visualType = base[state]; if (isString(visualType)) { var defa = visualDefault.get(visualType, 'active', isCategory); if (defa) { base[state] = {}; base[state][visualType] = defa; } else { // Mark as not specified. delete base[state]; } } }, this); } function completeInactive(base, stateExist, stateAbsent) { var optExist = base[stateExist]; var optAbsent = base[stateAbsent]; if (optExist && !optAbsent) { optAbsent = base[stateAbsent] = {}; each$26(optExist, function (visualData, visualType) { if (!VisualMapping.isValidType(visualType)) { return; } var defa = visualDefault.get(visualType, 'inactive', isCategory); if (defa != null) { optAbsent[visualType] = defa; // Compatibable with ec2: // Only inactive color to rgba(0,0,0,0) can not // make label transparent, so use opacity also. if (visualType === 'color' && !optAbsent.hasOwnProperty('opacity') && !optAbsent.hasOwnProperty('colorAlpha') ) { optAbsent.opacity = [0, 0]; } } }); } } function completeController(controller) { var symbolExists = (controller.inRange || {}).symbol || (controller.outOfRange || {}).symbol; var symbolSizeExists = (controller.inRange || {}).symbolSize || (controller.outOfRange || {}).symbolSize; var inactiveColor = this.get('inactiveColor'); each$26(this.stateList, function (state) { var itemSize = this.itemSize; var visuals = controller[state]; // Set inactive color for controller if no other color // attr (like colorAlpha) specified. if (!visuals) { visuals = controller[state] = { color: isCategory ? inactiveColor : [inactiveColor] }; } // Consistent symbol and symbolSize if not specified. if (visuals.symbol == null) { visuals.symbol = symbolExists && clone(symbolExists) || (isCategory ? 'roundRect' : ['roundRect']); } if (visuals.symbolSize == null) { visuals.symbolSize = symbolSizeExists && clone(symbolSizeExists) || (isCategory ? itemSize[0] : [itemSize[0], itemSize[0]]); } // Filter square and none. visuals.symbol = mapVisual$2(visuals.symbol, function (symbol) { return (symbol === 'none' || symbol === 'square') ? 'roundRect' : symbol; }); // Normalize symbolSize var symbolSize = visuals.symbolSize; if (symbolSize != null) { var max = -Infinity; // symbolSize can be object when categories defined. eachVisual(symbolSize, function (value) { value > max && (max = value); }); visuals.symbolSize = mapVisual$2(symbolSize, function (value) { return linearMap$3(value, [0, max], [0, itemSize[0]], true); }); } }, this); } }, /** * @protected */ resetItemSize: function () { this.itemSize = [ parseFloat(this.get('itemWidth')), parseFloat(this.get('itemHeight')) ]; }, /** * @public */ isCategory: function () { return !!this.option.categories; }, /** * @public * @abstract */ setSelected: noop$2, /** * @public * @abstract * @param {*|module:echarts/data/List} valueOrData * @param {number} dataIndex * @return {string} state See this.stateList */ getValueState: noop$2, /** * FIXME * Do not publish to thirt-part-dev temporarily * util the interface is stable. (Should it return * a function but not visual meta?) * * @pubilc * @abstract * @param {Function} getColorVisual * params: value, valueState * return: color * @return {Object} visualMeta * should includes {stops, outerColors} * outerColor means [colorBeyondMinValue, colorBeyondMaxValue] */ getVisualMeta: noop$2 }); // Constant var DEFAULT_BAR_BOUND = [20, 140]; var ContinuousModel = VisualMapModel.extend({ type: 'visualMap.continuous', /** * @protected */ defaultOption: { align: 'auto', // 'auto', 'left', 'right', 'top', 'bottom' calculable: false, // This prop effect default component type determine, // See echarts/component/visualMap/typeDefaulter. range: null, // selected range. In default case `range` is [min, max] // and can auto change along with modification of min max, // util use specifid a range. realtime: true, // Whether realtime update. itemHeight: null, // The length of the range control edge. itemWidth: null, // The length of the other side. hoverLink: true, // Enable hover highlight. hoverLinkDataSize: null,// The size of hovered data. hoverLinkOnHandle: null // Whether trigger hoverLink when hover handle. // If not specified, follow the value of `realtime`. }, /** * @override */ optionUpdated: function (newOption, isInit) { ContinuousModel.superApply(this, 'optionUpdated', arguments); this.resetExtent(); this.resetVisual(function (mappingOption) { mappingOption.mappingMethod = 'linear'; mappingOption.dataExtent = this.getExtent(); }); this._resetRange(); }, /** * @protected * @override */ resetItemSize: function () { ContinuousModel.superApply(this, 'resetItemSize', arguments); var itemSize = this.itemSize; this._orient === 'horizontal' && itemSize.reverse(); (itemSize[0] == null || isNaN(itemSize[0])) && (itemSize[0] = DEFAULT_BAR_BOUND[0]); (itemSize[1] == null || isNaN(itemSize[1])) && (itemSize[1] = DEFAULT_BAR_BOUND[1]); }, /** * @private */ _resetRange: function () { var dataExtent = this.getExtent(); var range = this.option.range; if (!range || range.auto) { // `range` should always be array (so we dont use other // value like 'auto') for user-friend. (consider getOption). dataExtent.auto = 1; this.option.range = dataExtent; } else if (isArray(range)) { if (range[0] > range[1]) { range.reverse(); } range[0] = Math.max(range[0], dataExtent[0]); range[1] = Math.min(range[1], dataExtent[1]); } }, /** * @protected * @override */ completeVisualOption: function () { VisualMapModel.prototype.completeVisualOption.apply(this, arguments); each$1(this.stateList, function (state) { var symbolSize = this.option.controller[state].symbolSize; if (symbolSize && symbolSize[0] !== symbolSize[1]) { symbolSize[0] = 0; // For good looking. } }, this); }, /** * @override */ setSelected: function (selected) { this.option.range = selected.slice(); this._resetRange(); }, /** * @public */ getSelected: function () { var dataExtent = this.getExtent(); var dataInterval = asc( (this.get('range') || []).slice() ); // Clamp dataInterval[0] > dataExtent[1] && (dataInterval[0] = dataExtent[1]); dataInterval[1] > dataExtent[1] && (dataInterval[1] = dataExtent[1]); dataInterval[0] < dataExtent[0] && (dataInterval[0] = dataExtent[0]); dataInterval[1] < dataExtent[0] && (dataInterval[1] = dataExtent[0]); return dataInterval; }, /** * @override */ getValueState: function (value) { var range = this.option.range; var dataExtent = this.getExtent(); // When range[0] === dataExtent[0], any value larger than dataExtent[0] maps to 'inRange'. // range[1] is processed likewise. return ( (range[0] <= dataExtent[0] || range[0] <= value) && (range[1] >= dataExtent[1] || value <= range[1]) ) ? 'inRange' : 'outOfRange'; }, /** * @params {Array.<number>} range target value: range[0] <= value && value <= range[1] * @return {Array.<Object>} [{seriesId, dataIndices: <Array.<number>>}, ...] */ findTargetDataIndices: function (range) { var result = []; this.eachTargetSeries(function (seriesModel) { var dataIndices = []; var data = seriesModel.getData(); data.each(this.getDataDimension(data), function (value, dataIndex) { range[0] <= value && value <= range[1] && dataIndices.push(dataIndex); }, this); result.push({seriesId: seriesModel.id, dataIndex: dataIndices}); }, this); return result; }, /** * @implement */ getVisualMeta: function (getColorVisual) { var oVals = getColorStopValues(this, 'outOfRange', this.getExtent()); var iVals = getColorStopValues(this, 'inRange', this.option.range.slice()); var stops = []; function setStop(value, valueState) { stops.push({ value: value, color: getColorVisual(value, valueState) }); } // Format to: outOfRange -- inRange -- outOfRange. var iIdx = 0; var oIdx = 0; var iLen = iVals.length; var oLen = oVals.length; for (; oIdx < oLen && (!iVals.length || oVals[oIdx] <= iVals[0]); oIdx++) { // If oVal[oIdx] === iVals[iIdx], oVal[oIdx] should be ignored. if (oVals[oIdx] < iVals[iIdx]) { setStop(oVals[oIdx], 'outOfRange'); } } for (var first = 1; iIdx < iLen; iIdx++, first = 0) { // If range is full, value beyond min, max will be clamped. // make a singularity first && stops.length && setStop(iVals[iIdx], 'outOfRange'); setStop(iVals[iIdx], 'inRange'); } for (var first = 1; oIdx < oLen; oIdx++) { if (!iVals.length || iVals[iVals.length - 1] < oVals[oIdx]) { // make a singularity if (first) { stops.length && setStop(stops[stops.length - 1].value, 'outOfRange'); first = 0; } setStop(oVals[oIdx], 'outOfRange'); } } var stopsLen = stops.length; return { stops: stops, outerColors: [ stopsLen ? stops[0].color : 'transparent', stopsLen ? stops[stopsLen - 1].color : 'transparent' ] }; } }); function getColorStopValues(visualMapModel, valueState, dataExtent) { if (dataExtent[0] === dataExtent[1]) { return dataExtent.slice(); } // When using colorHue mapping, it is not linear color any more. // Moreover, canvas gradient seems not to be accurate linear. // FIXME // Should be arbitrary value 100? or based on pixel size? var count = 200; var step = (dataExtent[1] - dataExtent[0]) / count; var value = dataExtent[0]; var stopValues = []; for (var i = 0; i <= count && value < dataExtent[1]; i++) { stopValues.push(value); value += step; } stopValues.push(dataExtent[1]); return stopValues; } var VisualMapView = extendComponentView({ type: 'visualMap', /** * @readOnly * @type {Object} */ autoPositionValues: {left: 1, right: 1, top: 1, bottom: 1}, init: function (ecModel, api) { /** * @readOnly * @type {module:echarts/model/Global} */ this.ecModel = ecModel; /** * @readOnly * @type {module:echarts/ExtensionAPI} */ this.api = api; /** * @readOnly * @type {module:echarts/component/visualMap/visualMapModel} */ this.visualMapModel; }, /** * @protected */ render: function (visualMapModel, ecModel, api, payload) { this.visualMapModel = visualMapModel; if (visualMapModel.get('show') === false) { this.group.removeAll(); return; } this.doRender.apply(this, arguments); }, /** * @protected */ renderBackground: function (group) { var visualMapModel = this.visualMapModel; var padding = normalizeCssArray$1(visualMapModel.get('padding') || 0); var rect = group.getBoundingRect(); group.add(new Rect({ z2: -1, // Lay background rect on the lowest layer. silent: true, shape: { x: rect.x - padding[3], y: rect.y - padding[0], width: rect.width + padding[3] + padding[1], height: rect.height + padding[0] + padding[2] }, style: { fill: visualMapModel.get('backgroundColor'), stroke: visualMapModel.get('borderColor'), lineWidth: visualMapModel.get('borderWidth') } })); }, /** * @protected * @param {number} targetValue can be Infinity or -Infinity * @param {string=} visualCluster Only can be 'color' 'opacity' 'symbol' 'symbolSize' * @param {Object} [opts] * @param {string=} [opts.forceState] Specify state, instead of using getValueState method. * @param {string=} [opts.convertOpacityToAlpha=false] For color gradient in controller widget. * @return {*} Visual value. */ getControllerVisual: function (targetValue, visualCluster, opts) { opts = opts || {}; var forceState = opts.forceState; var visualMapModel = this.visualMapModel; var visualObj = {}; // Default values. if (visualCluster === 'symbol') { visualObj.symbol = visualMapModel.get('itemSymbol'); } if (visualCluster === 'color') { var defaultColor = visualMapModel.get('contentColor'); visualObj.color = defaultColor; } function getter(key) { return visualObj[key]; } function setter(key, value) { visualObj[key] = value; } var mappings = visualMapModel.controllerVisuals[ forceState || visualMapModel.getValueState(targetValue) ]; var visualTypes = VisualMapping.prepareVisualTypes(mappings); each$1(visualTypes, function (type) { var visualMapping = mappings[type]; if (opts.convertOpacityToAlpha && type === 'opacity') { type = 'colorAlpha'; visualMapping = mappings.__alphaForOpacity; } if (VisualMapping.dependsOn(type, visualCluster)) { visualMapping && visualMapping.applyVisual( targetValue, getter, setter ); } }); return visualObj[visualCluster]; }, /** * @protected */ positionGroup: function (group) { var model = this.visualMapModel; var api = this.api; positionElement( group, model.getBoxLayoutParams(), {width: api.getWidth(), height: api.getHeight()} ); }, /** * @protected * @abstract */ doRender: noop }); /** * @param {module:echarts/component/visualMap/VisualMapModel} visualMapModel\ * @param {module:echarts/ExtensionAPI} api * @param {Array.<number>} itemSize always [short, long] * @return {string} 'left' or 'right' or 'top' or 'bottom' */ function getItemAlign(visualMapModel, api, itemSize) { var modelOption = visualMapModel.option; var itemAlign = modelOption.align; if (itemAlign != null && itemAlign !== 'auto') { return itemAlign; } // Auto decision align. var ecSize = {width: api.getWidth(), height: api.getHeight()}; var realIndex = modelOption.orient === 'horizontal' ? 1 : 0; var paramsSet = [ ['left', 'right', 'width'], ['top', 'bottom', 'height'] ]; var reals = paramsSet[realIndex]; var fakeValue = [0, null, 10]; var layoutInput = {}; for (var i = 0; i < 3; i++) { layoutInput[paramsSet[1 - realIndex][i]] = fakeValue[i]; layoutInput[reals[i]] = i === 2 ? itemSize[0] : modelOption[reals[i]]; } var rParam = [['x', 'width', 3], ['y', 'height', 0]][realIndex]; var rect = getLayoutRect(layoutInput, ecSize, modelOption.padding); return reals[ (rect.margin[rParam[2]] || 0) + rect[rParam[0]] + rect[rParam[1]] * 0.5 < ecSize[rParam[1]] * 0.5 ? 0 : 1 ]; } /** * Prepare dataIndex for outside usage, where dataIndex means rawIndex, and * dataIndexInside means filtered index. */ function convertDataIndex(batch) { each$1(batch || [], function (batchItem) { if (batch.dataIndex != null) { batch.dataIndexInside = batch.dataIndex; batch.dataIndex = null; } }); return batch; } var linearMap$4 = linearMap; var each$27 = each$1; var mathMin$7 = Math.min; var mathMax$7 = Math.max; // Arbitrary value var HOVER_LINK_SIZE = 12; var HOVER_LINK_OUT = 6; // Notice: // Any "interval" should be by the order of [low, high]. // "handle0" (handleIndex === 0) maps to // low data value: this._dataInterval[0] and has low coord. // "handle1" (handleIndex === 1) maps to // high data value: this._dataInterval[1] and has high coord. // The logic of transform is implemented in this._createBarGroup. var ContinuousView = VisualMapView.extend({ type: 'visualMap.continuous', /** * @override */ init: function () { ContinuousView.superApply(this, 'init', arguments); /** * @private */ this._shapes = {}; /** * @private */ this._dataInterval = []; /** * @private */ this._handleEnds = []; /** * @private */ this._orient; /** * @private */ this._useHandle; /** * @private */ this._hoverLinkDataIndices = []; /** * @private */ this._dragging; /** * @private */ this._hovering; }, /** * @protected * @override */ doRender: function (visualMapModel, ecModel, api, payload) { if (!payload || payload.type !== 'selectDataRange' || payload.from !== this.uid) { this._buildView(); } }, /** * @private */ _buildView: function () { this.group.removeAll(); var visualMapModel = this.visualMapModel; var thisGroup = this.group; this._orient = visualMapModel.get('orient'); this._useHandle = visualMapModel.get('calculable'); this._resetInterval(); this._renderBar(thisGroup); var dataRangeText = visualMapModel.get('text'); this._renderEndsText(thisGroup, dataRangeText, 0); this._renderEndsText(thisGroup, dataRangeText, 1); // Do this for background size calculation. this._updateView(true); // After updating view, inner shapes is built completely, // and then background can be rendered. this.renderBackground(thisGroup); // Real update view this._updateView(); this._enableHoverLinkToSeries(); this._enableHoverLinkFromSeries(); this.positionGroup(thisGroup); }, /** * @private */ _renderEndsText: function (group, dataRangeText, endsIndex) { if (!dataRangeText) { return; } // Compatible with ec2, text[0] map to high value, text[1] map low value. var text = dataRangeText[1 - endsIndex]; text = text != null ? text + '' : ''; var visualMapModel = this.visualMapModel; var textGap = visualMapModel.get('textGap'); var itemSize = visualMapModel.itemSize; var barGroup = this._shapes.barGroup; var position = this._applyTransform( [ itemSize[0] / 2, endsIndex === 0 ? -textGap : itemSize[1] + textGap ], barGroup ); var align = this._applyTransform( endsIndex === 0 ? 'bottom' : 'top', barGroup ); var orient = this._orient; var textStyleModel = this.visualMapModel.textStyleModel; this.group.add(new Text({ style: { x: position[0], y: position[1], textVerticalAlign: orient === 'horizontal' ? 'middle' : align, textAlign: orient === 'horizontal' ? align : 'center', text: text, textFont: textStyleModel.getFont(), textFill: textStyleModel.getTextColor() } })); }, /** * @private */ _renderBar: function (targetGroup) { var visualMapModel = this.visualMapModel; var shapes = this._shapes; var itemSize = visualMapModel.itemSize; var orient = this._orient; var useHandle = this._useHandle; var itemAlign = getItemAlign(visualMapModel, this.api, itemSize); var barGroup = shapes.barGroup = this._createBarGroup(itemAlign); // Bar barGroup.add(shapes.outOfRange = createPolygon()); barGroup.add(shapes.inRange = createPolygon( null, useHandle ? getCursor$1(this._orient) : null, bind(this._dragHandle, this, 'all', false), bind(this._dragHandle, this, 'all', true) )); var textRect = visualMapModel.textStyleModel.getTextRect('国'); var textSize = mathMax$7(textRect.width, textRect.height); // Handle if (useHandle) { shapes.handleThumbs = []; shapes.handleLabels = []; shapes.handleLabelPoints = []; this._createHandle(barGroup, 0, itemSize, textSize, orient, itemAlign); this._createHandle(barGroup, 1, itemSize, textSize, orient, itemAlign); } this._createIndicator(barGroup, itemSize, textSize, orient); targetGroup.add(barGroup); }, /** * @private */ _createHandle: function (barGroup, handleIndex, itemSize, textSize, orient) { var onDrift = bind(this._dragHandle, this, handleIndex, false); var onDragEnd = bind(this._dragHandle, this, handleIndex, true); var handleThumb = createPolygon( createHandlePoints(handleIndex, textSize), getCursor$1(this._orient), onDrift, onDragEnd ); handleThumb.position[0] = itemSize[0]; barGroup.add(handleThumb); // Text is always horizontal layout but should not be effected by // transform (orient/inverse). So label is built separately but not // use zrender/graphic/helper/RectText, and is located based on view // group (according to handleLabelPoint) but not barGroup. var textStyleModel = this.visualMapModel.textStyleModel; var handleLabel = new Text({ draggable: true, drift: onDrift, onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, ondragend: onDragEnd, style: { x: 0, y: 0, text: '', textFont: textStyleModel.getFont(), textFill: textStyleModel.getTextColor() } }); this.group.add(handleLabel); var handleLabelPoint = [ orient === 'horizontal' ? textSize / 2 : textSize * 1.5, orient === 'horizontal' ? (handleIndex === 0 ? -(textSize * 1.5) : (textSize * 1.5)) : (handleIndex === 0 ? -textSize / 2 : textSize / 2) ]; var shapes = this._shapes; shapes.handleThumbs[handleIndex] = handleThumb; shapes.handleLabelPoints[handleIndex] = handleLabelPoint; shapes.handleLabels[handleIndex] = handleLabel; }, /** * @private */ _createIndicator: function (barGroup, itemSize, textSize, orient) { var indicator = createPolygon([[0, 0]], 'move'); indicator.position[0] = itemSize[0]; indicator.attr({invisible: true, silent: true}); barGroup.add(indicator); var textStyleModel = this.visualMapModel.textStyleModel; var indicatorLabel = new Text({ silent: true, invisible: true, style: { x: 0, y: 0, text: '', textFont: textStyleModel.getFont(), textFill: textStyleModel.getTextColor() } }); this.group.add(indicatorLabel); var indicatorLabelPoint = [ orient === 'horizontal' ? textSize / 2 : HOVER_LINK_OUT + 3, 0 ]; var shapes = this._shapes; shapes.indicator = indicator; shapes.indicatorLabel = indicatorLabel; shapes.indicatorLabelPoint = indicatorLabelPoint; }, /** * @private */ _dragHandle: function (handleIndex, isEnd, dx, dy) { if (!this._useHandle) { return; } this._dragging = !isEnd; if (!isEnd) { // Transform dx, dy to bar coordination. var vertex = this._applyTransform([dx, dy], this._shapes.barGroup, true); this._updateInterval(handleIndex, vertex[1]); // Considering realtime, update view should be executed // before dispatch action. this._updateView(); } // dragEnd do not dispatch action when realtime. if (isEnd === !this.visualMapModel.get('realtime')) { // jshint ignore:line this.api.dispatchAction({ type: 'selectDataRange', from: this.uid, visualMapId: this.visualMapModel.id, selected: this._dataInterval.slice() }); } if (isEnd) { !this._hovering && this._clearHoverLinkToSeries(); } else if (useHoverLinkOnHandle(this.visualMapModel)) { this._doHoverLinkToSeries(this._handleEnds[handleIndex], false); } }, /** * @private */ _resetInterval: function () { var visualMapModel = this.visualMapModel; var dataInterval = this._dataInterval = visualMapModel.getSelected(); var dataExtent = visualMapModel.getExtent(); var sizeExtent = [0, visualMapModel.itemSize[1]]; this._handleEnds = [ linearMap$4(dataInterval[0], dataExtent, sizeExtent, true), linearMap$4(dataInterval[1], dataExtent, sizeExtent, true) ]; }, /** * @private * @param {(number|string)} handleIndex 0 or 1 or 'all' * @param {number} dx * @param {number} dy */ _updateInterval: function (handleIndex, delta) { delta = delta || 0; var visualMapModel = this.visualMapModel; var handleEnds = this._handleEnds; var sizeExtent = [0, visualMapModel.itemSize[1]]; sliderMove( delta, handleEnds, sizeExtent, handleIndex, // cross is forbiden 0 ); var dataExtent = visualMapModel.getExtent(); // Update data interval. this._dataInterval = [ linearMap$4(handleEnds[0], sizeExtent, dataExtent, true), linearMap$4(handleEnds[1], sizeExtent, dataExtent, true) ]; }, /** * @private */ _updateView: function (forSketch) { var visualMapModel = this.visualMapModel; var dataExtent = visualMapModel.getExtent(); var shapes = this._shapes; var outOfRangeHandleEnds = [0, visualMapModel.itemSize[1]]; var inRangeHandleEnds = forSketch ? outOfRangeHandleEnds : this._handleEnds; var visualInRange = this._createBarVisual( this._dataInterval, dataExtent, inRangeHandleEnds, 'inRange' ); var visualOutOfRange = this._createBarVisual( dataExtent, dataExtent, outOfRangeHandleEnds, 'outOfRange' ); shapes.inRange .setStyle({ fill: visualInRange.barColor, opacity: visualInRange.opacity }) .setShape('points', visualInRange.barPoints); shapes.outOfRange .setStyle({ fill: visualOutOfRange.barColor, opacity: visualOutOfRange.opacity }) .setShape('points', visualOutOfRange.barPoints); this._updateHandle(inRangeHandleEnds, visualInRange); }, /** * @private */ _createBarVisual: function (dataInterval, dataExtent, handleEnds, forceState) { var opts = { forceState: forceState, convertOpacityToAlpha: true }; var colorStops = this._makeColorGradient(dataInterval, opts); var symbolSizes = [ this.getControllerVisual(dataInterval[0], 'symbolSize', opts), this.getControllerVisual(dataInterval[1], 'symbolSize', opts) ]; var barPoints = this._createBarPoints(handleEnds, symbolSizes); return { barColor: new LinearGradient(0, 0, 0, 1, colorStops), barPoints: barPoints, handlesColor: [ colorStops[0].color, colorStops[colorStops.length - 1].color ] }; }, /** * @private */ _makeColorGradient: function (dataInterval, opts) { // Considering colorHue, which is not linear, so we have to sample // to calculate gradient color stops, but not only caculate head // and tail. var sampleNumber = 100; // Arbitrary value. var colorStops = []; var step = (dataInterval[1] - dataInterval[0]) / sampleNumber; colorStops.push({ color: this.getControllerVisual(dataInterval[0], 'color', opts), offset: 0 }); for (var i = 1; i < sampleNumber; i++) { var currValue = dataInterval[0] + step * i; if (currValue > dataInterval[1]) { break; } colorStops.push({ color: this.getControllerVisual(currValue, 'color', opts), offset: i / sampleNumber }); } colorStops.push({ color: this.getControllerVisual(dataInterval[1], 'color', opts), offset: 1 }); return colorStops; }, /** * @private */ _createBarPoints: function (handleEnds, symbolSizes) { var itemSize = this.visualMapModel.itemSize; return [ [itemSize[0] - symbolSizes[0], handleEnds[0]], [itemSize[0], handleEnds[0]], [itemSize[0], handleEnds[1]], [itemSize[0] - symbolSizes[1], handleEnds[1]] ]; }, /** * @private */ _createBarGroup: function (itemAlign) { var orient = this._orient; var inverse = this.visualMapModel.get('inverse'); return new Group( (orient === 'horizontal' && !inverse) ? {scale: itemAlign === 'bottom' ? [1, 1] : [-1, 1], rotation: Math.PI / 2} : (orient === 'horizontal' && inverse) ? {scale: itemAlign === 'bottom' ? [-1, 1] : [1, 1], rotation: -Math.PI / 2} : (orient === 'vertical' && !inverse) ? {scale: itemAlign === 'left' ? [1, -1] : [-1, -1]} : {scale: itemAlign === 'left' ? [1, 1] : [-1, 1]} ); }, /** * @private */ _updateHandle: function (handleEnds, visualInRange) { if (!this._useHandle) { return; } var shapes = this._shapes; var visualMapModel = this.visualMapModel; var handleThumbs = shapes.handleThumbs; var handleLabels = shapes.handleLabels; each$27([0, 1], function (handleIndex) { var handleThumb = handleThumbs[handleIndex]; handleThumb.setStyle('fill', visualInRange.handlesColor[handleIndex]); handleThumb.position[1] = handleEnds[handleIndex]; // Update handle label position. var textPoint = applyTransform$1( shapes.handleLabelPoints[handleIndex], getTransform(handleThumb, this.group) ); handleLabels[handleIndex].setStyle({ x: textPoint[0], y: textPoint[1], text: visualMapModel.formatValueText(this._dataInterval[handleIndex]), textVerticalAlign: 'middle', textAlign: this._applyTransform( this._orient === 'horizontal' ? (handleIndex === 0 ? 'bottom' : 'top') : 'left', shapes.barGroup ) }); }, this); }, /** * @private * @param {number} cursorValue * @param {number} textValue * @param {string} [rangeSymbol] * @param {number} [halfHoverLinkSize] */ _showIndicator: function (cursorValue, textValue, rangeSymbol, halfHoverLinkSize) { var visualMapModel = this.visualMapModel; var dataExtent = visualMapModel.getExtent(); var itemSize = visualMapModel.itemSize; var sizeExtent = [0, itemSize[1]]; var pos = linearMap$4(cursorValue, dataExtent, sizeExtent, true); var shapes = this._shapes; var indicator = shapes.indicator; if (!indicator) { return; } indicator.position[1] = pos; indicator.attr('invisible', false); indicator.setShape('points', createIndicatorPoints( !!rangeSymbol, halfHoverLinkSize, pos, itemSize[1] )); var opts = {convertOpacityToAlpha: true}; var color = this.getControllerVisual(cursorValue, 'color', opts); indicator.setStyle('fill', color); // Update handle label position. var textPoint = applyTransform$1( shapes.indicatorLabelPoint, getTransform(indicator, this.group) ); var indicatorLabel = shapes.indicatorLabel; indicatorLabel.attr('invisible', false); var align = this._applyTransform('left', shapes.barGroup); var orient = this._orient; indicatorLabel.setStyle({ text: (rangeSymbol ? rangeSymbol : '') + visualMapModel.formatValueText(textValue), textVerticalAlign: orient === 'horizontal' ? align : 'middle', textAlign: orient === 'horizontal' ? 'center' : align, x: textPoint[0], y: textPoint[1] }); }, /** * @private */ _enableHoverLinkToSeries: function () { var self = this; this._shapes.barGroup .on('mousemove', function (e) { self._hovering = true; if (!self._dragging) { var itemSize = self.visualMapModel.itemSize; var pos = self._applyTransform( [e.offsetX, e.offsetY], self._shapes.barGroup, true, true ); // For hover link show when hover handle, which might be // below or upper than sizeExtent. pos[1] = mathMin$7(mathMax$7(0, pos[1]), itemSize[1]); self._doHoverLinkToSeries( pos[1], 0 <= pos[0] && pos[0] <= itemSize[0] ); } }) .on('mouseout', function () { // When mouse is out of handle, hoverLink still need // to be displayed when realtime is set as false. self._hovering = false; !self._dragging && self._clearHoverLinkToSeries(); }); }, /** * @private */ _enableHoverLinkFromSeries: function () { var zr = this.api.getZr(); if (this.visualMapModel.option.hoverLink) { zr.on('mouseover', this._hoverLinkFromSeriesMouseOver, this); zr.on('mouseout', this._hideIndicator, this); } else { this._clearHoverLinkFromSeries(); } }, /** * @private */ _doHoverLinkToSeries: function (cursorPos, hoverOnBar) { var visualMapModel = this.visualMapModel; var itemSize = visualMapModel.itemSize; if (!visualMapModel.option.hoverLink) { return; } var sizeExtent = [0, itemSize[1]]; var dataExtent = visualMapModel.getExtent(); // For hover link show when hover handle, which might be below or upper than sizeExtent. cursorPos = mathMin$7(mathMax$7(sizeExtent[0], cursorPos), sizeExtent[1]); var halfHoverLinkSize = getHalfHoverLinkSize(visualMapModel, dataExtent, sizeExtent); var hoverRange = [cursorPos - halfHoverLinkSize, cursorPos + halfHoverLinkSize]; var cursorValue = linearMap$4(cursorPos, sizeExtent, dataExtent, true); var valueRange = [ linearMap$4(hoverRange[0], sizeExtent, dataExtent, true), linearMap$4(hoverRange[1], sizeExtent, dataExtent, true) ]; // Consider data range is out of visualMap range, see test/visualMap-continuous.html, // where china and india has very large population. hoverRange[0] < sizeExtent[0] && (valueRange[0] = -Infinity); hoverRange[1] > sizeExtent[1] && (valueRange[1] = Infinity); // Do not show indicator when mouse is over handle, // otherwise labels overlap, especially when dragging. if (hoverOnBar) { if (valueRange[0] === -Infinity) { this._showIndicator(cursorValue, valueRange[1], '< ', halfHoverLinkSize); } else if (valueRange[1] === Infinity) { this._showIndicator(cursorValue, valueRange[0], '> ', halfHoverLinkSize); } else { this._showIndicator(cursorValue, cursorValue, '≈ ', halfHoverLinkSize); } } // When realtime is set as false, handles, which are in barGroup, // also trigger hoverLink, which help user to realize where they // focus on when dragging. (see test/heatmap-large.html) // When realtime is set as true, highlight will not show when hover // handle, because the label on handle, which displays a exact value // but not range, might mislead users. var oldBatch = this._hoverLinkDataIndices; var newBatch = []; if (hoverOnBar || useHoverLinkOnHandle(visualMapModel)) { newBatch = this._hoverLinkDataIndices = visualMapModel.findTargetDataIndices(valueRange); } var resultBatches = compressBatches(oldBatch, newBatch); this._dispatchHighDown('downplay', convertDataIndex(resultBatches[0])); this._dispatchHighDown('highlight', convertDataIndex(resultBatches[1])); }, /** * @private */ _hoverLinkFromSeriesMouseOver: function (e) { var el = e.target; var visualMapModel = this.visualMapModel; if (!el || el.dataIndex == null) { return; } var dataModel = this.ecModel.getSeriesByIndex(el.seriesIndex); if (!visualMapModel.isTargetSeries(dataModel)) { return; } var data = dataModel.getData(el.dataType); var value = data.get(visualMapModel.getDataDimension(data), el.dataIndex, true); if (!isNaN(value)) { this._showIndicator(value, value); } }, /** * @private */ _hideIndicator: function () { var shapes = this._shapes; shapes.indicator && shapes.indicator.attr('invisible', true); shapes.indicatorLabel && shapes.indicatorLabel.attr('invisible', true); }, /** * @private */ _clearHoverLinkToSeries: function () { this._hideIndicator(); var indices = this._hoverLinkDataIndices; this._dispatchHighDown('downplay', convertDataIndex(indices)); indices.length = 0; }, /** * @private */ _clearHoverLinkFromSeries: function () { this._hideIndicator(); var zr = this.api.getZr(); zr.off('mouseover', this._hoverLinkFromSeriesMouseOver); zr.off('mouseout', this._hideIndicator); }, /** * @private */ _applyTransform: function (vertex, element, inverse, global) { var transform = getTransform(element, global ? null : this.group); return graphic[ isArray(vertex) ? 'applyTransform' : 'transformDirection' ](vertex, transform, inverse); }, /** * @private */ _dispatchHighDown: function (type, batch) { batch && batch.length && this.api.dispatchAction({ type: type, batch: batch }); }, /** * @override */ dispose: function () { this._clearHoverLinkFromSeries(); this._clearHoverLinkToSeries(); }, /** * @override */ remove: function () { this._clearHoverLinkFromSeries(); this._clearHoverLinkToSeries(); } }); function createPolygon(points, cursor, onDrift, onDragEnd) { return new Polygon({ shape: {points: points}, draggable: !!onDrift, cursor: cursor, drift: onDrift, onmousemove: function (e) { // Fot mobile devicem, prevent screen slider on the button. stop(e.event); }, ondragend: onDragEnd }); } function createHandlePoints(handleIndex, textSize) { return handleIndex === 0 ? [[0, 0], [textSize, 0], [textSize, -textSize]] : [[0, 0], [textSize, 0], [textSize, textSize]]; } function createIndicatorPoints(isRange, halfHoverLinkSize, pos, extentMax) { return isRange ? [ // indicate range [0, -mathMin$7(halfHoverLinkSize, mathMax$7(pos, 0))], [HOVER_LINK_OUT, 0], [0, mathMin$7(halfHoverLinkSize, mathMax$7(extentMax - pos, 0))] ] : [ // indicate single value [0, 0], [5, -5], [5, 5] ]; } function getHalfHoverLinkSize(visualMapModel, dataExtent, sizeExtent) { var halfHoverLinkSize = HOVER_LINK_SIZE / 2; var hoverLinkDataSize = visualMapModel.get('hoverLinkDataSize'); if (hoverLinkDataSize) { halfHoverLinkSize = linearMap$4(hoverLinkDataSize, dataExtent, sizeExtent, true) / 2; } return halfHoverLinkSize; } function useHoverLinkOnHandle(visualMapModel) { var hoverLinkOnHandle = visualMapModel.get('hoverLinkOnHandle'); return !!(hoverLinkOnHandle == null ? visualMapModel.get('realtime') : hoverLinkOnHandle); } function getCursor$1(orient) { return orient === 'vertical' ? 'ns-resize' : 'ew-resize'; } var actionInfo$2 = { type: 'selectDataRange', event: 'dataRangeSelected', // FIXME use updateView appears wrong update: 'update' }; registerAction(actionInfo$2, function (payload, ecModel) { ecModel.eachComponent({mainType: 'visualMap', query: payload}, function (model) { model.setSelected(payload.selected); }); }); /** * DataZoom component entry */ registerPreprocessor(preprocessor$2); var PiecewiseModel = VisualMapModel.extend({ type: 'visualMap.piecewise', /** * Order Rule: * * option.categories / option.pieces / option.text / option.selected: * If !option.inverse, * Order when vertical: ['top', ..., 'bottom']. * Order when horizontal: ['left', ..., 'right']. * If option.inverse, the meaning of * the order should be reversed. * * this._pieceList: * The order is always [low, ..., high]. * * Mapping from location to low-high: * If !option.inverse * When vertical, top is high. * When horizontal, right is high. * If option.inverse, reverse. */ /** * @protected */ defaultOption: { selected: null, // Object. If not specified, means selected. // When pieces and splitNumber: {'0': true, '5': true} // When categories: {'cate1': false, 'cate3': true} // When selected === false, means all unselected. minOpen: false, // Whether include values that smaller than `min`. maxOpen: false, // Whether include values that bigger than `max`. align: 'auto', // 'auto', 'left', 'right' itemWidth: 20, // When put the controller vertically, it is the length of // horizontal side of each item. Otherwise, vertical side. itemHeight: 14, // When put the controller vertically, it is the length of // vertical side of each item. Otherwise, horizontal side. itemSymbol: 'roundRect', pieceList: null, // Each item is Object, with some of those attrs: // {min, max, lt, gt, lte, gte, value, // color, colorSaturation, colorAlpha, opacity, // symbol, symbolSize}, which customize the range or visual // coding of the certain piece. Besides, see "Order Rule". categories: null, // category names, like: ['some1', 'some2', 'some3']. // Attr min/max are ignored when categories set. See "Order Rule" splitNumber: 5, // If set to 5, auto split five pieces equally. // If set to 0 and component type not set, component type will be // determined as "continuous". (It is less reasonable but for ec2 // compatibility, see echarts/component/visualMap/typeDefaulter) selectedMode: 'multiple', // Can be 'multiple' or 'single'. itemGap: 10, // The gap between two items, in px. hoverLink: true, // Enable hover highlight. showLabel: null // By default, when text is used, label will hide (the logic // is remained for compatibility reason) }, /** * @override */ optionUpdated: function (newOption, isInit) { PiecewiseModel.superApply(this, 'optionUpdated', arguments); /** * The order is always [low, ..., high]. * [{text: string, interval: Array.<number>}, ...] * @private * @type {Array.<Object>} */ this._pieceList = []; this.resetExtent(); /** * 'pieces', 'categories', 'splitNumber' * @type {string} */ var mode = this._mode = this._determineMode(); resetMethods[this._mode].call(this); this._resetSelected(newOption, isInit); var categories = this.option.categories; this.resetVisual(function (mappingOption, state) { if (mode === 'categories') { mappingOption.mappingMethod = 'category'; mappingOption.categories = clone(categories); } else { mappingOption.dataExtent = this.getExtent(); mappingOption.mappingMethod = 'piecewise'; mappingOption.pieceList = map(this._pieceList, function (piece) { var piece = clone(piece); if (state !== 'inRange') { // FIXME // outOfRange do not support special visual in pieces. piece.visual = null; } return piece; }); } }); }, /** * @protected * @override */ completeVisualOption: function () { // Consider this case: // visualMap: { // pieces: [{symbol: 'circle', lt: 0}, {symbol: 'rect', gte: 0}] // } // where no inRange/outOfRange set but only pieces. So we should make // default inRange/outOfRange for this case, otherwise visuals that only // appear in `pieces` will not be taken into account in visual encoding. var option = this.option; var visualTypesInPieces = {}; var visualTypes = VisualMapping.listVisualTypes(); var isCategory = this.isCategory(); each$1(option.pieces, function (piece) { each$1(visualTypes, function (visualType) { if (piece.hasOwnProperty(visualType)) { visualTypesInPieces[visualType] = 1; } }); }); each$1(visualTypesInPieces, function (v, visualType) { var exists = 0; each$1(this.stateList, function (state) { exists |= has(option, state, visualType) || has(option.target, state, visualType); }, this); !exists && each$1(this.stateList, function (state) { (option[state] || (option[state] = {}))[visualType] = visualDefault.get( visualType, state === 'inRange' ? 'active' : 'inactive', isCategory ); }); }, this); function has(obj, state, visualType) { return obj && obj[state] && ( isObject$1(obj[state]) ? obj[state].hasOwnProperty(visualType) : obj[state] === visualType // e.g., inRange: 'symbol' ); } VisualMapModel.prototype.completeVisualOption.apply(this, arguments); }, _resetSelected: function (newOption, isInit) { var thisOption = this.option; var pieceList = this._pieceList; // Selected do not merge but all override. var selected = (isInit ? thisOption : newOption).selected || {}; thisOption.selected = selected; // Consider 'not specified' means true. each$1(pieceList, function (piece, index) { var key = this.getSelectedMapKey(piece); if (!selected.hasOwnProperty(key)) { selected[key] = true; } }, this); if (thisOption.selectedMode === 'single') { // Ensure there is only one selected. var hasSel = false; each$1(pieceList, function (piece, index) { var key = this.getSelectedMapKey(piece); if (selected[key]) { hasSel ? (selected[key] = false) : (hasSel = true); } }, this); } // thisOption.selectedMode === 'multiple', default: all selected. }, /** * @public */ getSelectedMapKey: function (piece) { return this._mode === 'categories' ? piece.value + '' : piece.index + ''; }, /** * @public */ getPieceList: function () { return this._pieceList; }, /** * @private * @return {string} */ _determineMode: function () { var option = this.option; return option.pieces && option.pieces.length > 0 ? 'pieces' : this.option.categories ? 'categories' : 'splitNumber'; }, /** * @public * @override */ setSelected: function (selected) { this.option.selected = clone(selected); }, /** * @public * @override */ getValueState: function (value) { var index = VisualMapping.findPieceIndex(value, this._pieceList); return index != null ? (this.option.selected[this.getSelectedMapKey(this._pieceList[index])] ? 'inRange' : 'outOfRange' ) : 'outOfRange'; }, /** * @public * @params {number} pieceIndex piece index in visualMapModel.getPieceList() * @return {Array.<Object>} [{seriesId, dataIndices: <Array.<number>>}, ...] */ findTargetDataIndices: function (pieceIndex) { var result = []; this.eachTargetSeries(function (seriesModel) { var dataIndices = []; var data = seriesModel.getData(); data.each(this.getDataDimension(data), function (value, dataIndex) { // Should always base on model pieceList, because it is order sensitive. var pIdx = VisualMapping.findPieceIndex(value, this._pieceList); pIdx === pieceIndex && dataIndices.push(dataIndex); }, this); result.push({seriesId: seriesModel.id, dataIndex: dataIndices}); }, this); return result; }, /** * @private * @param {Object} piece piece.value or piece.interval is required. * @return {number} Can be Infinity or -Infinity */ getRepresentValue: function (piece) { var representValue; if (this.isCategory()) { representValue = piece.value; } else { if (piece.value != null) { representValue = piece.value; } else { var pieceInterval = piece.interval || []; representValue = (pieceInterval[0] === -Infinity && pieceInterval[1] === Infinity) ? 0 : (pieceInterval[0] + pieceInterval[1]) / 2; } } return representValue; }, getVisualMeta: function (getColorVisual) { // Do not support category. (category axis is ordinal, numerical) if (this.isCategory()) { return; } var stops = []; var outerColors = []; var visualMapModel = this; function setStop(interval, valueState) { var representValue = visualMapModel.getRepresentValue({interval: interval}); if (!valueState) { valueState = visualMapModel.getValueState(representValue); } var color = getColorVisual(representValue, valueState); if (interval[0] === -Infinity) { outerColors[0] = color; } else if (interval[1] === Infinity) { outerColors[1] = color; } else { stops.push( {value: interval[0], color: color}, {value: interval[1], color: color} ); } } // Suplement var pieceList = this._pieceList.slice(); if (!pieceList.length) { pieceList.push({interval: [-Infinity, Infinity]}); } else { var edge = pieceList[0].interval[0]; edge !== -Infinity && pieceList.unshift({interval: [-Infinity, edge]}); edge = pieceList[pieceList.length - 1].interval[1]; edge !== Infinity && pieceList.push({interval: [edge, Infinity]}); } var curr = -Infinity; each$1(pieceList, function (piece) { var interval = piece.interval; if (interval) { // Fulfill gap. interval[0] > curr && setStop([curr, interval[0]], 'outOfRange'); setStop(interval.slice()); curr = interval[1]; } }, this); return {stops: stops, outerColors: outerColors}; } }); /** * Key is this._mode * @type {Object} * @this {module:echarts/component/viusalMap/PiecewiseMode} */ var resetMethods = { splitNumber: function () { var thisOption = this.option; var pieceList = this._pieceList; var precision = Math.min(thisOption.precision, 20); var dataExtent = this.getExtent(); var splitNumber = thisOption.splitNumber; splitNumber = Math.max(parseInt(splitNumber, 10), 1); thisOption.splitNumber = splitNumber; var splitStep = (dataExtent[1] - dataExtent[0]) / splitNumber; // Precision auto-adaption while (+splitStep.toFixed(precision) !== splitStep && precision < 5) { precision++; } thisOption.precision = precision; splitStep = +splitStep.toFixed(precision); var index = 0; if (thisOption.minOpen) { pieceList.push({ index: index++, interval: [-Infinity, dataExtent[0]], close: [0, 0] }); } for ( var curr = dataExtent[0], len = index + splitNumber; index < len; curr += splitStep ) { var max = index === splitNumber - 1 ? dataExtent[1] : (curr + splitStep); pieceList.push({ index: index++, interval: [curr, max], close: [1, 1] }); } if (thisOption.maxOpen) { pieceList.push({ index: index++, interval: [dataExtent[1], Infinity], close: [0, 0] }); } reformIntervals(pieceList); each$1(pieceList, function (piece) { piece.text = this.formatValueText(piece.interval); }, this); }, categories: function () { var thisOption = this.option; each$1(thisOption.categories, function (cate) { // FIXME category模式也使用pieceList,但在visualMapping中不是使用pieceList。 // 是否改一致。 this._pieceList.push({ text: this.formatValueText(cate, true), value: cate }); }, this); // See "Order Rule". normalizeReverse(thisOption, this._pieceList); }, pieces: function () { var thisOption = this.option; var pieceList = this._pieceList; each$1(thisOption.pieces, function (pieceListItem, index) { if (!isObject$1(pieceListItem)) { pieceListItem = {value: pieceListItem}; } var item = {text: '', index: index}; if (pieceListItem.label != null) { item.text = pieceListItem.label; } if (pieceListItem.hasOwnProperty('value')) { var value = item.value = pieceListItem.value; item.interval = [value, value]; item.close = [1, 1]; } else { // `min` `max` is legacy option. // `lt` `gt` `lte` `gte` is recommanded. var interval = item.interval = []; var close = item.close = [0, 0]; var closeList = [1, 0, 1]; var infinityList = [-Infinity, Infinity]; var useMinMax = []; for (var lg = 0; lg < 2; lg++) { var names = [['gte', 'gt', 'min'], ['lte', 'lt', 'max']][lg]; for (var i = 0; i < 3 && interval[lg] == null; i++) { interval[lg] = pieceListItem[names[i]]; close[lg] = closeList[i]; useMinMax[lg] = i === 2; } interval[lg] == null && (interval[lg] = infinityList[lg]); } useMinMax[0] && interval[1] === Infinity && (close[0] = 0); useMinMax[1] && interval[0] === -Infinity && (close[1] = 0); if (__DEV__) { if (interval[0] > interval[1]) { console.warn( 'Piece ' + index + 'is illegal: ' + interval + ' lower bound should not greater then uppper bound.' ); } } if (interval[0] === interval[1] && close[0] && close[1]) { // Consider: [{min: 5, max: 5, visual: {...}}, {min: 0, max: 5}], // we use value to lift the priority when min === max item.value = interval[0]; } } item.visual = VisualMapping.retrieveVisuals(pieceListItem); pieceList.push(item); }, this); // See "Order Rule". normalizeReverse(thisOption, pieceList); // Only pieces reformIntervals(pieceList); each$1(pieceList, function (piece) { var close = piece.close; var edgeSymbols = [['<', '≤'][close[1]], ['>', '≥'][close[0]]]; piece.text = piece.text || this.formatValueText( piece.value != null ? piece.value : piece.interval, false, edgeSymbols ); }, this); } }; function normalizeReverse(thisOption, pieceList) { var inverse = thisOption.inverse; if (thisOption.orient === 'vertical' ? !inverse : inverse) { pieceList.reverse(); } } var PiecewiseVisualMapView = VisualMapView.extend({ type: 'visualMap.piecewise', /** * @protected * @override */ doRender: function () { var thisGroup = this.group; thisGroup.removeAll(); var visualMapModel = this.visualMapModel; var textGap = visualMapModel.get('textGap'); var textStyleModel = visualMapModel.textStyleModel; var textFont = textStyleModel.getFont(); var textFill = textStyleModel.getTextColor(); var itemAlign = this._getItemAlign(); var itemSize = visualMapModel.itemSize; var viewData = this._getViewData(); var endsText = viewData.endsText; var showLabel = retrieve(visualMapModel.get('showLabel', true), !endsText); endsText && this._renderEndsText( thisGroup, endsText[0], itemSize, showLabel, itemAlign ); each$1(viewData.viewPieceList, renderItem, this); endsText && this._renderEndsText( thisGroup, endsText[1], itemSize, showLabel, itemAlign ); box( visualMapModel.get('orient'), thisGroup, visualMapModel.get('itemGap') ); this.renderBackground(thisGroup); this.positionGroup(thisGroup); function renderItem(item) { var piece = item.piece; var itemGroup = new Group(); itemGroup.onclick = bind(this._onItemClick, this, piece); this._enableHoverLink(itemGroup, item.indexInModelPieceList); var representValue = visualMapModel.getRepresentValue(piece); this._createItemSymbol( itemGroup, representValue, [0, 0, itemSize[0], itemSize[1]] ); if (showLabel) { var visualState = this.visualMapModel.getValueState(representValue); itemGroup.add(new Text({ style: { x: itemAlign === 'right' ? -textGap : itemSize[0] + textGap, y: itemSize[1] / 2, text: piece.text, textVerticalAlign: 'middle', textAlign: itemAlign, textFont: textFont, textFill: textFill, opacity: visualState === 'outOfRange' ? 0.5 : 1 } })); } thisGroup.add(itemGroup); } }, /** * @private */ _enableHoverLink: function (itemGroup, pieceIndex) { itemGroup .on('mouseover', bind(onHoverLink, this, 'highlight')) .on('mouseout', bind(onHoverLink, this, 'downplay')); function onHoverLink(method) { var visualMapModel = this.visualMapModel; visualMapModel.option.hoverLink && this.api.dispatchAction({ type: method, batch: convertDataIndex( visualMapModel.findTargetDataIndices(pieceIndex) ) }); } }, /** * @private */ _getItemAlign: function () { var visualMapModel = this.visualMapModel; var modelOption = visualMapModel.option; if (modelOption.orient === 'vertical') { return getItemAlign( visualMapModel, this.api, visualMapModel.itemSize ); } else { // horizontal, most case left unless specifying right. var align = modelOption.align; if (!align || align === 'auto') { align = 'left'; } return align; } }, /** * @private */ _renderEndsText: function (group, text, itemSize, showLabel, itemAlign) { if (!text) { return; } var itemGroup = new Group(); var textStyleModel = this.visualMapModel.textStyleModel; itemGroup.add(new Text({ style: { x: showLabel ? (itemAlign === 'right' ? itemSize[0] : 0) : itemSize[0] / 2, y: itemSize[1] / 2, textVerticalAlign: 'middle', textAlign: showLabel ? itemAlign : 'center', text: text, textFont: textStyleModel.getFont(), textFill: textStyleModel.getTextColor() } })); group.add(itemGroup); }, /** * @private * @return {Object} {peiceList, endsText} The order is the same as screen pixel order. */ _getViewData: function () { var visualMapModel = this.visualMapModel; var viewPieceList = map(visualMapModel.getPieceList(), function (piece, index) { return {piece: piece, indexInModelPieceList: index}; }); var endsText = visualMapModel.get('text'); // Consider orient and inverse. var orient = visualMapModel.get('orient'); var inverse = visualMapModel.get('inverse'); // Order of model pieceList is always [low, ..., high] if (orient === 'horizontal' ? inverse : !inverse) { viewPieceList.reverse(); } // Origin order of endsText is [high, low] else if (endsText) { endsText = endsText.slice().reverse(); } return {viewPieceList: viewPieceList, endsText: endsText}; }, /** * @private */ _createItemSymbol: function (group, representValue, shapeParam) { group.add(createSymbol( this.getControllerVisual(representValue, 'symbol'), shapeParam[0], shapeParam[1], shapeParam[2], shapeParam[3], this.getControllerVisual(representValue, 'color') )); }, /** * @private */ _onItemClick: function (piece) { var visualMapModel = this.visualMapModel; var option = visualMapModel.option; var selected = clone(option.selected); var newKey = visualMapModel.getSelectedMapKey(piece); if (option.selectedMode === 'single') { selected[newKey] = true; each$1(selected, function (o, key) { selected[key] = key === newKey; }); } else { selected[newKey] = !selected[newKey]; } this.api.dispatchAction({ type: 'selectDataRange', from: this.uid, visualMapId: this.visualMapModel.id, selected: selected }); } }); /** * DataZoom component entry */ registerPreprocessor(preprocessor$2); /** * visualMap component entry */ var addCommas$1 = addCommas; var encodeHTML$1 = encodeHTML; function fillLabel(opt) { defaultEmphasis(opt, 'label', ['show']); } var MarkerModel = extendComponentModel({ type: 'marker', dependencies: ['series', 'grid', 'polar', 'geo'], /** * @overrite */ init: function (option, parentModel, ecModel, extraOpt) { if (__DEV__) { if (this.type === 'marker') { throw new Error('Marker component is abstract component. Use markLine, markPoint, markArea instead.'); } } this.mergeDefaultAndTheme(option, ecModel); this.mergeOption(option, ecModel, extraOpt.createdBySelf, true); }, /** * @return {boolean} */ isAnimationEnabled: function () { if (env$1.node) { return false; } var hostSeries = this.__hostSeries; return this.getShallow('animation') && hostSeries && hostSeries.isAnimationEnabled(); }, mergeOption: function (newOpt, ecModel, createdBySelf, isInit) { var MarkerModel = this.constructor; var modelPropName = this.mainType + 'Model'; if (!createdBySelf) { ecModel.eachSeries(function (seriesModel) { var markerOpt = seriesModel.get(this.mainType); var markerModel = seriesModel[modelPropName]; if (!markerOpt || !markerOpt.data) { seriesModel[modelPropName] = null; return; } if (!markerModel) { if (isInit) { // Default label emphasis `position` and `show` fillLabel(markerOpt); } each$1(markerOpt.data, function (item) { // FIXME Overwrite fillLabel method ? if (item instanceof Array) { fillLabel(item[0]); fillLabel(item[1]); } else { fillLabel(item); } }); markerModel = new MarkerModel( markerOpt, this, ecModel ); extend(markerModel, { mainType: this.mainType, // Use the same series index and name seriesIndex: seriesModel.seriesIndex, name: seriesModel.name, createdBySelf: true }); markerModel.__hostSeries = seriesModel; } else { markerModel.mergeOption(markerOpt, ecModel, true); } seriesModel[modelPropName] = markerModel; }, this); } }, formatTooltip: function (dataIndex) { var data = this.getData(); var value = this.getRawValue(dataIndex); var formattedValue = isArray(value) ? map(value, addCommas$1).join(', ') : addCommas$1(value); var name = data.getName(dataIndex); var html = encodeHTML$1(this.name); if (value != null || name) { html += '<br />'; } if (name) { html += encodeHTML$1(name); if (value != null) { html += ' : '; } } if (value != null) { html += encodeHTML$1(formattedValue); } return html; }, getData: function () { return this._data; }, setData: function (data) { this._data = data; } }); mixin(MarkerModel, dataFormatMixin); MarkerModel.extend({ type: 'markPoint', defaultOption: { zlevel: 0, z: 5, symbol: 'pin', symbolSize: 50, //symbolRotate: 0, //symbolOffset: [0, 0] tooltip: { trigger: 'item' }, label: { show: true, position: 'inside' }, itemStyle: { borderWidth: 2 }, emphasis: { label: { show: true } } } }); var indexOf$2 = indexOf; function hasXOrY(item) { return !(isNaN(parseFloat(item.x)) && isNaN(parseFloat(item.y))); } function hasXAndY(item) { return !isNaN(parseFloat(item.x)) && !isNaN(parseFloat(item.y)); } // Make it simple, do not visit all stacked value to count precision. // function getPrecision(data, valueAxisDim, dataIndex) { // var precision = -1; // var stackedDim = data.mapDimension(valueAxisDim); // do { // precision = Math.max( // numberUtil.getPrecision(data.get(stackedDim, dataIndex)), // precision // ); // var stackedOnSeries = data.getCalculationInfo('stackedOnSeries'); // if (stackedOnSeries) { // var byValue = data.get(data.getCalculationInfo('stackedByDimension'), dataIndex); // data = stackedOnSeries.getData(); // dataIndex = data.indexOf(data.getCalculationInfo('stackedByDimension'), byValue); // stackedDim = data.getCalculationInfo('stackedDimension'); // } // else { // data = null; // } // } while (data); // return precision; // } function markerTypeCalculatorWithExtent( mlType, data, otherDataDim, targetDataDim, otherCoordIndex, targetCoordIndex ) { var coordArr = []; var stacked = isDimensionStacked(data, targetDataDim, otherDataDim); var calcDataDim = stacked ? data.getCalculationInfo('stackResultDimension') : targetDataDim; var value = numCalculate(data, calcDataDim, mlType); var dataIndex = data.indicesOfNearest(calcDataDim, value)[0]; coordArr[otherCoordIndex] = data.get(otherDataDim, dataIndex); coordArr[targetCoordIndex] = data.get(targetDataDim, dataIndex); // Make it simple, do not visit all stacked value to count precision. var precision = getPrecision(data.get(targetDataDim, dataIndex)); precision = Math.min(precision, 20); if (precision >= 0) { coordArr[targetCoordIndex] = +coordArr[targetCoordIndex].toFixed(precision); } return coordArr; } var curry$7 = curry; // TODO Specified percent var markerTypeCalculator = { /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ min: curry$7(markerTypeCalculatorWithExtent, 'min'), /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ max: curry$7(markerTypeCalculatorWithExtent, 'max'), /** * @method * @param {module:echarts/data/List} data * @param {string} baseAxisDim * @param {string} valueAxisDim */ average: curry$7(markerTypeCalculatorWithExtent, 'average') }; /** * Transform markPoint data item to format used in List by do the following * 1. Calculate statistic like `max`, `min`, `average` * 2. Convert `item.xAxis`, `item.yAxis` to `item.coord` array * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/coord/*} [coordSys] * @param {Object} item * @return {Object} */ function dataTransform(seriesModel, item) { var data = seriesModel.getData(); var coordSys = seriesModel.coordinateSystem; // 1. If not specify the position with pixel directly // 2. If `coord` is not a data array. Which uses `xAxis`, // `yAxis` to specify the coord on each dimension // parseFloat first because item.x and item.y can be percent string like '20%' if (item && !hasXAndY(item) && !isArray(item.coord) && coordSys) { var dims = coordSys.dimensions; var axisInfo = getAxisInfo$1(item, data, coordSys, seriesModel); // Clone the option // Transform the properties xAxis, yAxis, radiusAxis, angleAxis, geoCoord to value item = clone(item); if (item.type && markerTypeCalculator[item.type] && axisInfo.baseAxis && axisInfo.valueAxis ) { var otherCoordIndex = indexOf$2(dims, axisInfo.baseAxis.dim); var targetCoordIndex = indexOf$2(dims, axisInfo.valueAxis.dim); item.coord = markerTypeCalculator[item.type]( data, axisInfo.baseDataDim, axisInfo.valueDataDim, otherCoordIndex, targetCoordIndex ); // Force to use the value of calculated value. item.value = item.coord[targetCoordIndex]; } else { // FIXME Only has one of xAxis and yAxis. var coord = [ item.xAxis != null ? item.xAxis : item.radiusAxis, item.yAxis != null ? item.yAxis : item.angleAxis ]; // Each coord support max, min, average for (var i = 0; i < 2; i++) { if (markerTypeCalculator[coord[i]]) { coord[i] = numCalculate(data, data.mapDimension(dims[i]), coord[i]); } } item.coord = coord; } } return item; } function getAxisInfo$1(item, data, coordSys, seriesModel) { var ret = {}; if (item.valueIndex != null || item.valueDim != null) { ret.valueDataDim = item.valueIndex != null ? data.getDimension(item.valueIndex) : item.valueDim; ret.valueAxis = coordSys.getAxis(dataDimToCoordDim(seriesModel, ret.valueDataDim)); ret.baseAxis = coordSys.getOtherAxis(ret.valueAxis); ret.baseDataDim = data.mapDimension(ret.baseAxis.dim); } else { ret.baseAxis = seriesModel.getBaseAxis(); ret.valueAxis = coordSys.getOtherAxis(ret.baseAxis); ret.baseDataDim = data.mapDimension(ret.baseAxis.dim); ret.valueDataDim = data.mapDimension(ret.valueAxis.dim); } return ret; } function dataDimToCoordDim(seriesModel, dataDim) { var data = seriesModel.getData(); var dimensions = data.dimensions; dataDim = data.getDimension(dataDim); for (var i = 0; i < dimensions.length; i++) { var dimItem = data.getDimensionInfo(dimensions[i]); if (dimItem.name === dataDim) { return dimItem.coordDim; } } } /** * Filter data which is out of coordinateSystem range * [dataFilter description] * @param {module:echarts/coord/*} [coordSys] * @param {Object} item * @return {boolean} */ function dataFilter$1(coordSys, item) { // Alwalys return true if there is no coordSys return (coordSys && coordSys.containData && item.coord && !hasXOrY(item)) ? coordSys.containData(item.coord) : true; } function dimValueGetter(item, dimName, dataIndex, dimIndex) { // x, y, radius, angle if (dimIndex < 2) { return item.coord && item.coord[dimIndex]; } return item.value; } function numCalculate(data, valueDataDim, type) { if (type === 'average') { var sum = 0; var count = 0; data.each(valueDataDim, function (val, idx) { if (!isNaN(val)) { sum += val; count++; } }); return sum / count; } else { return data.getDataExtent(valueDataDim, true)[type === 'max' ? 1 : 0]; } } var MarkerView = extendComponentView({ type: 'marker', init: function () { /** * Markline grouped by series * @private * @type {module:zrender/core/util.HashMap} */ this.markerGroupMap = createHashMap(); }, render: function (markerModel, ecModel, api) { var markerGroupMap = this.markerGroupMap; markerGroupMap.each(function (item) { item.__keep = false; }); var markerModelKey = this.type + 'Model'; ecModel.eachSeries(function (seriesModel) { var markerModel = seriesModel[markerModelKey]; markerModel && this.renderSeries(seriesModel, markerModel, ecModel, api); }, this); markerGroupMap.each(function (item) { !item.__keep && this.group.remove(item.group); }, this); }, renderSeries: function () {} }); function updateMarkerLayout(mpData, seriesModel, api) { var coordSys = seriesModel.coordinateSystem; mpData.each(function (idx) { var itemModel = mpData.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get('x'), api.getWidth()); var yPx = parsePercent$1(itemModel.get('y'), api.getHeight()); if (!isNaN(xPx) && !isNaN(yPx)) { point = [xPx, yPx]; } // Chart like bar may have there own marker positioning logic else if (seriesModel.getMarkerPosition) { // Use the getMarkerPoisition point = seriesModel.getMarkerPosition( mpData.getValues(mpData.dimensions, idx) ); } else if (coordSys) { var x = mpData.get(coordSys.dimensions[0], idx); var y = mpData.get(coordSys.dimensions[1], idx); point = coordSys.dataToPoint([x, y]); } // Use x, y if has any if (!isNaN(xPx)) { point[0] = xPx; } if (!isNaN(yPx)) { point[1] = yPx; } mpData.setItemLayout(idx, point); }); } MarkerView.extend({ type: 'markPoint', // updateLayout: function (markPointModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var mpModel = seriesModel.markPointModel; // if (mpModel) { // updateMarkerLayout(mpModel.getData(), seriesModel, api); // this.markerGroupMap.get(seriesModel.id).updateLayout(mpModel); // } // }, this); // }, updateTransform: function (markPointModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var mpModel = seriesModel.markPointModel; if (mpModel) { updateMarkerLayout(mpModel.getData(), seriesModel, api); this.markerGroupMap.get(seriesModel.id).updateLayout(mpModel); } }, this); }, renderSeries: function (seriesModel, mpModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesId = seriesModel.id; var seriesData = seriesModel.getData(); var symbolDrawMap = this.markerGroupMap; var symbolDraw = symbolDrawMap.get(seriesId) || symbolDrawMap.set(seriesId, new SymbolDraw()); var mpData = createList$1(coordSys, seriesModel, mpModel); // FIXME mpModel.setData(mpData); updateMarkerLayout(mpModel.getData(), seriesModel, api); mpData.each(function (idx) { var itemModel = mpData.getItemModel(idx); var symbolSize = itemModel.getShallow('symbolSize'); if (typeof symbolSize === 'function') { // FIXME 这里不兼容 ECharts 2.x,2.x 貌似参数是整个数据? symbolSize = symbolSize( mpModel.getRawValue(idx), mpModel.getDataParams(idx) ); } mpData.setItemVisual(idx, { symbolSize: symbolSize, color: itemModel.get('itemStyle.color') || seriesData.getVisual('color'), symbol: itemModel.getShallow('symbol') }); }); // TODO Text are wrong symbolDraw.updateData(mpData); this.group.add(symbolDraw.group); // Set host model for tooltip // FIXME mpData.eachItemGraphicEl(function (el) { el.traverse(function (child) { child.dataModel = mpModel; }); }); symbolDraw.__keep = true; symbolDraw.group.silent = mpModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} [coordSys] * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$1(coordSys, seriesModel, mpModel) { var coordDimsInfos; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var 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 defaults({name: coordDim}, info); }); } else { coordDimsInfos =[{ name: 'value', type: 'float' }]; } var mpData = new List(coordDimsInfos, mpModel); var dataOpt = map(mpModel.get('data'), curry( dataTransform, seriesModel )); if (coordSys) { dataOpt = filter( dataOpt, curry(dataFilter$1, coordSys) ); } mpData.initData(dataOpt, null, coordSys ? dimValueGetter : function (item) { return item.value; } ); return mpData; } // HINT Markpoint can't be used too much registerPreprocessor(function (opt) { // Make sure markPoint component is enabled opt.markPoint = opt.markPoint || {}; }); MarkerModel.extend({ type: 'markLine', defaultOption: { zlevel: 0, z: 5, symbol: ['circle', 'arrow'], symbolSize: [8, 16], //symbolRotate: 0, precision: 2, tooltip: { trigger: 'item' }, label: { show: true, position: 'end' }, lineStyle: { type: 'dashed' }, emphasis: { label: { show: true }, lineStyle: { width: 3 } }, animationEasing: 'linear' } }); var markLineTransform = function (seriesModel, coordSys, mlModel, item) { var data = seriesModel.getData(); // Special type markLine like 'min', 'max', 'average' var mlType = item.type; if (!isArray(item) && ( mlType === 'min' || mlType === 'max' || mlType === 'average' // In case // data: [{ // yAxis: 10 // }] || (item.xAxis != null || item.yAxis != null) ) ) { var valueAxis; var valueDataDim; var value; if (item.yAxis != null || item.xAxis != null) { valueDataDim = item.yAxis != null ? 'y' : 'x'; valueAxis = coordSys.getAxis(valueDataDim); value = retrieve(item.yAxis, item.xAxis); } else { var axisInfo = getAxisInfo$1(item, data, coordSys, seriesModel); valueDataDim = axisInfo.valueDataDim; valueAxis = axisInfo.valueAxis; value = numCalculate(data, valueDataDim, mlType); } var valueIndex = valueDataDim === 'x' ? 0 : 1; var baseIndex = 1 - valueIndex; var mlFrom = clone(item); var mlTo = {}; mlFrom.type = null; mlFrom.coord = []; mlTo.coord = []; mlFrom.coord[baseIndex] = -Infinity; mlTo.coord[baseIndex] = Infinity; var precision = mlModel.get('precision'); if (precision >= 0 && typeof value === 'number') { value = +value.toFixed(Math.min(precision, 20)); } mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value; item = [mlFrom, mlTo, { // Extra option for tooltip and label type: mlType, valueIndex: item.valueIndex, // Force to use the value of calculated value. value: value }]; } item = [ dataTransform(seriesModel, item[0]), dataTransform(seriesModel, item[1]), extend({}, item[2]) ]; // Avoid line data type is extended by from(to) data type item[2].type = item[2].type || ''; // Merge from option and to option into line option merge(item[2], item[0]); merge(item[2], item[1]); return item; }; function isInifinity(val) { return !isNaN(val) && !isFinite(val); } // If a markLine has one dim function ifMarkLineHasOnlyDim(dimIndex, fromCoord, toCoord, coordSys) { var otherDimIndex = 1 - dimIndex; var dimName = coordSys.dimensions[dimIndex]; return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex]) && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]); } function markLineFilter(coordSys, item) { if (coordSys.type === 'cartesian2d') { var fromCoord = item[0].coord; var 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 dataFilter$1(coordSys, item[0]) && dataFilter$1(coordSys, item[1]); } function updateSingleMarkerEndLayout( data, idx, isFrom, seriesModel, api ) { var coordSys = seriesModel.coordinateSystem; var itemModel = data.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get('x'), api.getWidth()); var yPx = parsePercent$1(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 getMarkerPoisition point = seriesModel.getMarkerPosition( data.getValues(data.dimensions, idx) ); } else { var dims = coordSys.dimensions; var x = data.get(dims[0], idx); var 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 (coordSys.type === 'cartesian2d') { var xAxis = coordSys.getAxis('x'); var yAxis = coordSys.getAxis('y'); var dims = coordSys.dimensions; if (isInifinity(data.get(dims[0], idx))) { point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]); } else if (isInifinity(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); } MarkerView.extend({ type: 'markLine', // updateLayout: function (markLineModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var mlModel = seriesModel.markLineModel; // if (mlModel) { // var mlData = mlModel.getData(); // var fromData = mlModel.__from; // var toData = 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); // }, updateTransform: function (markLineModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var mlModel = seriesModel.markLineModel; if (mlModel) { var mlData = mlModel.getData(); var fromData = mlModel.__from; var toData = 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: function (seriesModel, mlModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesId = seriesModel.id; var seriesData = seriesModel.getData(); var lineDrawMap = this.markerGroupMap; var lineDraw = lineDrawMap.get(seriesId) || lineDrawMap.set(seriesId, new LineDraw()); this.group.add(lineDraw.group); var mlData = createList$2(coordSys, seriesModel, mlModel); var fromData = mlData.from; var toData = mlData.to; var lineData = mlData.line; mlModel.__from = fromData; mlModel.__to = toData; // Line data for tooltip and formatter mlModel.setData(lineData); var symbolType = mlModel.get('symbol'); var symbolSize = mlModel.get('symbolSize'); if (!isArray(symbolType)) { symbolType = [symbolType, symbolType]; } if (typeof symbolSize === 'number') { symbolSize = [symbolSize, symbolSize]; } // 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) { var lineColor = lineData.getItemModel(idx).get('lineStyle.color'); lineData.setItemVisual(idx, { color: lineColor || fromData.getItemVisual(idx, 'color') }); lineData.setItemLayout(idx, [ fromData.getItemLayout(idx), toData.getItemLayout(idx) ]); lineData.setItemVisual(idx, { 'fromSymbolSize': fromData.getItemVisual(idx, 'symbolSize'), 'fromSymbol': fromData.getItemVisual(idx, 'symbol'), 'toSymbolSize': toData.getItemVisual(idx, 'symbolSize'), 'toSymbol': toData.getItemVisual(idx, 'symbol') }); }); lineDraw.updateData(lineData); // Set host model for tooltip // FIXME mlData.line.eachItemGraphicEl(function (el, idx) { el.traverse(function (child) { child.dataModel = mlModel; }); }); function updateDataVisualAndLayout(data, idx, isFrom) { var itemModel = data.getItemModel(idx); updateSingleMarkerEndLayout( data, idx, isFrom, seriesModel, api ); data.setItemVisual(idx, { symbolSize: itemModel.get('symbolSize') || symbolSize[isFrom ? 0 : 1], symbol: itemModel.get('symbol', true) || symbolType[isFrom ? 0 : 1], color: itemModel.get('itemStyle.color') || seriesData.getVisual('color') }); } lineDraw.__keep = true; lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} coordSys * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$2(coordSys, seriesModel, mlModel) { var coordDimsInfos; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var 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 defaults({name: coordDim}, info); }); } else { coordDimsInfos =[{ name: 'value', type: 'float' }]; } var fromData = new List(coordDimsInfos, mlModel); var toData = new List(coordDimsInfos, mlModel); // No dimensions var lineData = new List([], mlModel); var optData = map(mlModel.get('data'), curry( markLineTransform, seriesModel, coordSys, mlModel )); if (coordSys) { optData = filter( optData, curry(markLineFilter, coordSys) ); } var dimValueGetter$$1 = coordSys ? dimValueGetter : function (item) { return item.value; }; fromData.initData( map(optData, function (item) { return item[0]; }), null, dimValueGetter$$1 ); toData.initData( map(optData, function (item) { return item[1]; }), null, dimValueGetter$$1 ); lineData.initData( map(optData, function (item) { return item[2]; }) ); lineData.hasItemOption = true; return { from: fromData, to: toData, line: lineData }; } registerPreprocessor(function (opt) { // Make sure markLine component is enabled opt.markLine = opt.markLine || {}; }); MarkerModel.extend({ type: 'markArea', defaultOption: { zlevel: 0, // PENDING z: 1, tooltip: { trigger: 'item' }, // markArea should fixed on the coordinate system animation: false, label: { show: true, position: 'top' }, itemStyle: { // color and borderColor default to use color from series // color: 'auto' // borderColor: 'auto' borderWidth: 0 }, emphasis: { label: { show: true, position: 'top' } } } }); // TODO Better on polar var markAreaTransform = function (seriesModel, coordSys, maModel, item) { var lt = dataTransform(seriesModel, item[0]); var rb = dataTransform(seriesModel, item[1]); var retrieve$$1 = retrieve; // FIXME make sure lt is less than rb var ltCoord = lt.coord; var rbCoord = rb.coord; ltCoord[0] = retrieve$$1(ltCoord[0], -Infinity); ltCoord[1] = retrieve$$1(ltCoord[1], -Infinity); rbCoord[0] = retrieve$$1(rbCoord[0], Infinity); rbCoord[1] = retrieve$$1(rbCoord[1], Infinity); // Merge option into one var result = mergeAll([{}, lt, rb]); result.coord = [ lt.coord, rb.coord ]; result.x0 = lt.x; result.y0 = lt.y; result.x1 = rb.x; result.y1 = rb.y; return result; }; function isInifinity$1(val) { return !isNaN(val) && !isFinite(val); } // If a markArea has one dim function ifMarkLineHasOnlyDim$1(dimIndex, fromCoord, toCoord, coordSys) { var otherDimIndex = 1 - dimIndex; return isInifinity$1(fromCoord[otherDimIndex]) && isInifinity$1(toCoord[otherDimIndex]); } function markAreaFilter(coordSys, item) { var fromCoord = item.coord[0]; var toCoord = item.coord[1]; if (coordSys.type === 'cartesian2d') { // In case // { // markArea: { // data: [{ yAxis: 2 }] // } // } if ( fromCoord && toCoord && (ifMarkLineHasOnlyDim$1(1, fromCoord, toCoord, coordSys) || ifMarkLineHasOnlyDim$1(0, fromCoord, toCoord, coordSys)) ) { return true; } } return dataFilter$1(coordSys, { coord: fromCoord, x: item.x0, y: item.y0 }) || dataFilter$1(coordSys, { coord: toCoord, x: item.x1, y: item.y1 }); } // dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0'] function getSingleMarkerEndPoint(data, idx, dims, seriesModel, api) { var coordSys = seriesModel.coordinateSystem; var itemModel = data.getItemModel(idx); var point; var xPx = parsePercent$1(itemModel.get(dims[0]), api.getWidth()); var yPx = parsePercent$1(itemModel.get(dims[1]), 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 getMarkerPoisition point = seriesModel.getMarkerPosition( data.getValues(dims, idx) ); } else { var x = data.get(dims[0], idx); var y = data.get(dims[1], idx); var pt = [x, y]; coordSys.clampData && coordSys.clampData(pt, pt); point = coordSys.dataToPoint(pt, true); } if (coordSys.type === 'cartesian2d') { var xAxis = coordSys.getAxis('x'); var yAxis = coordSys.getAxis('y'); var x = data.get(dims[0], idx); var y = data.get(dims[1], idx); if (isInifinity$1(x)) { point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[dims[0] === 'x0' ? 0 : 1]); } else if (isInifinity$1(y)) { point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[dims[1] === 'y0' ? 0 : 1]); } } // Use x, y if has any if (!isNaN(xPx)) { point[0] = xPx; } if (!isNaN(yPx)) { point[1] = yPx; } } return point; } var dimPermutations = [['x0', 'y0'], ['x1', 'y0'], ['x1', 'y1'], ['x0', 'y1']]; MarkerView.extend({ type: 'markArea', // updateLayout: function (markAreaModel, ecModel, api) { // ecModel.eachSeries(function (seriesModel) { // var maModel = seriesModel.markAreaModel; // if (maModel) { // var areaData = maModel.getData(); // areaData.each(function (idx) { // var points = zrUtil.map(dimPermutations, function (dim) { // return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); // }); // // Layout // areaData.setItemLayout(idx, points); // var el = areaData.getItemGraphicEl(idx); // el.setShape('points', points); // }); // } // }, this); // }, updateTransform: function (markAreaModel, ecModel, api) { ecModel.eachSeries(function (seriesModel) { var maModel = seriesModel.markAreaModel; if (maModel) { var areaData = maModel.getData(); areaData.each(function (idx) { var points = map(dimPermutations, function (dim) { return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); }); // Layout areaData.setItemLayout(idx, points); var el = areaData.getItemGraphicEl(idx); el.setShape('points', points); }); } }, this); }, renderSeries: function (seriesModel, maModel, ecModel, api) { var coordSys = seriesModel.coordinateSystem; var seriesName = seriesModel.name; var seriesData = seriesModel.getData(); var areaGroupMap = this.markerGroupMap; var polygonGroup = areaGroupMap.get(seriesName) || areaGroupMap.set(seriesName, {group: new Group()}); this.group.add(polygonGroup.group); polygonGroup.__keep = true; var areaData = createList$3(coordSys, seriesModel, maModel); // Line data for tooltip and formatter maModel.setData(areaData); // Update visual and layout of line areaData.each(function (idx) { // Layout areaData.setItemLayout(idx, map(dimPermutations, function (dim) { return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); })); // Visual areaData.setItemVisual(idx, { color: seriesData.getVisual('color') }); }); areaData.diff(polygonGroup.__data) .add(function (idx) { var polygon = new Polygon({ shape: { points: areaData.getItemLayout(idx) } }); areaData.setItemGraphicEl(idx, polygon); polygonGroup.group.add(polygon); }) .update(function (newIdx, oldIdx) { var polygon = polygonGroup.__data.getItemGraphicEl(oldIdx); updateProps(polygon, { shape: { points: areaData.getItemLayout(newIdx) } }, maModel, newIdx); polygonGroup.group.add(polygon); areaData.setItemGraphicEl(newIdx, polygon); }) .remove(function (idx) { var polygon = polygonGroup.__data.getItemGraphicEl(idx); polygonGroup.group.remove(polygon); }) .execute(); areaData.eachItemGraphicEl(function (polygon, idx) { var itemModel = areaData.getItemModel(idx); var labelModel = itemModel.getModel('label'); var labelHoverModel = itemModel.getModel('emphasis.label'); var color = areaData.getItemVisual(idx, 'color'); polygon.useStyle( defaults( itemModel.getModel('itemStyle').getItemStyle(), { fill: modifyAlpha(color, 0.4), stroke: color } ) ); polygon.hoverStyle = itemModel.getModel('emphasis.itemStyle').getItemStyle(); setLabelStyle( polygon.style, polygon.hoverStyle, labelModel, labelHoverModel, { labelFetcher: maModel, labelDataIndex: idx, defaultText: areaData.getName(idx) || '', isRectText: true, autoColor: color } ); setHoverStyle(polygon, {}); polygon.dataModel = maModel; }); polygonGroup.__data = areaData; polygonGroup.group.silent = maModel.get('silent') || seriesModel.get('silent'); } }); /** * @inner * @param {module:echarts/coord/*} coordSys * @param {module:echarts/model/Series} seriesModel * @param {module:echarts/model/Model} mpModel */ function createList$3(coordSys, seriesModel, maModel) { var coordDimsInfos; var areaData; var dims = ['x0', 'y0', 'x1', 'y1']; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { var data = seriesModel.getData(); var info = data.getDimensionInfo( data.mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys return defaults({name: coordDim}, info); }); areaData = new List(map(dims, function (dim, idx) { return { name: dim, type: coordDimsInfos[idx % 2].type }; }), maModel); } else { coordDimsInfos =[{ name: 'value', type: 'float' }]; areaData = new List(coordDimsInfos, maModel); } var optData = map(maModel.get('data'), curry( markAreaTransform, seriesModel, coordSys, maModel )); if (coordSys) { optData = filter( optData, curry(markAreaFilter, coordSys) ); } var dimValueGetter$$1 = coordSys ? function (item, dimName, dataIndex, dimIndex) { return item.coord[Math.floor(dimIndex / 2)][dimIndex % 2]; } : function (item) { return item.value; }; areaData.initData(optData, null, dimValueGetter$$1); areaData.hasItemOption = true; return areaData; } registerPreprocessor(function (opt) { // Make sure markArea component is enabled opt.markArea = opt.markArea || {}; }); var preprocessor$3 = function (option) { var timelineOpt = option && option.timeline; if (!isArray(timelineOpt)) { timelineOpt = timelineOpt ? [timelineOpt] : []; } each$1(timelineOpt, function (opt) { if (!opt) { return; } compatibleEC2(opt); }); }; function compatibleEC2(opt) { var type = opt.type; var ec2Types = {'number': 'value', 'time': 'time'}; // Compatible with ec2 if (ec2Types[type]) { opt.axisType = ec2Types[type]; delete opt.type; } transferItem(opt); if (has$2(opt, 'controlPosition')) { var controlStyle = opt.controlStyle || (opt.controlStyle = {}); if (!has$2(controlStyle, 'position')) { controlStyle.position = opt.controlPosition; } if (controlStyle.position === 'none' && !has$2(controlStyle, 'show')) { controlStyle.show = false; delete controlStyle.position; } delete opt.controlPosition; } each$1(opt.data || [], function (dataItem) { if (isObject$1(dataItem) && !isArray(dataItem)) { if (!has$2(dataItem, 'value') && has$2(dataItem, 'name')) { // In ec2, using name as value. dataItem.value = dataItem.name; } transferItem(dataItem); } }); } function transferItem(opt) { var itemStyle = opt.itemStyle || (opt.itemStyle = {}); var itemStyleEmphasis = itemStyle.emphasis || (itemStyle.emphasis = {}); // Transfer label out var label = opt.label || (opt.label || {}); var labelNormal = label.normal || (label.normal = {}); var excludeLabelAttr = {normal: 1, emphasis: 1}; each$1(label, function (value, name) { if (!excludeLabelAttr[name] && !has$2(labelNormal, name)) { labelNormal[name] = value; } }); if (itemStyleEmphasis.label && !has$2(label, 'emphasis')) { label.emphasis = itemStyleEmphasis.label; delete itemStyleEmphasis.label; } } function has$2(obj, attr) { return obj.hasOwnProperty(attr); } ComponentModel.registerSubTypeDefaulter('timeline', function () { // Only slider now. return 'slider'; }); registerAction( {type: 'timelineChange', event: 'timelineChanged', update: 'prepareAndUpdate'}, function (payload, ecModel) { var timelineModel = ecModel.getComponent('timeline'); if (timelineModel && payload.currentIndex != null) { timelineModel.setCurrentIndex(payload.currentIndex); if (!timelineModel.get('loop', true) && timelineModel.isIndexMax()) { timelineModel.setPlayState(false); } } // Set normalized currentIndex to payload. ecModel.resetOption('timeline'); return defaults({ currentIndex: timelineModel.option.currentIndex }, payload); } ); registerAction( {type: 'timelinePlayChange', event: 'timelinePlayChanged', update: 'update'}, function (payload, ecModel) { var timelineModel = ecModel.getComponent('timeline'); if (timelineModel && payload.playState != null) { timelineModel.setPlayState(payload.playState); } } ); var TimelineModel = ComponentModel.extend({ type: 'timeline', layoutMode: 'box', /** * @protected */ defaultOption: { zlevel: 0, // 一级层叠 z: 4, // 二级层叠 show: true, axisType: 'time', // 模式是时间类型,支持 value, category realtime: true, left: '20%', top: null, right: '20%', bottom: 0, width: null, height: 40, padding: 5, controlPosition: 'left', // 'left' 'right' 'top' 'bottom' 'none' autoPlay: false, rewind: false, // 反向播放 loop: true, playInterval: 2000, // 播放时间间隔,单位ms currentIndex: 0, itemStyle: {}, label: { color: '#000' }, data: [] }, /** * @override */ init: function (option, parentModel, ecModel) { /** * @private * @type {module:echarts/data/List} */ this._data; /** * @private * @type {Array.<string>} */ this._names; this.mergeDefaultAndTheme(option, ecModel); this._initData(); }, /** * @override */ mergeOption: function (option) { TimelineModel.superApply(this, 'mergeOption', arguments); this._initData(); }, /** * @param {number} [currentIndex] */ setCurrentIndex: function (currentIndex) { if (currentIndex == null) { currentIndex = this.option.currentIndex; } var count = this._data.count(); if (this.option.loop) { currentIndex = (currentIndex % count + count) % count; } else { currentIndex >= count && (currentIndex = count - 1); currentIndex < 0 && (currentIndex = 0); } this.option.currentIndex = currentIndex; }, /** * @return {number} currentIndex */ getCurrentIndex: function () { return this.option.currentIndex; }, /** * @return {boolean} */ isIndexMax: function () { return this.getCurrentIndex() >= this._data.count() - 1; }, /** * @param {boolean} state true: play, false: stop */ setPlayState: function (state) { this.option.autoPlay = !!state; }, /** * @return {boolean} true: play, false: stop */ getPlayState: function () { return !!this.option.autoPlay; }, /** * @private */ _initData: function () { var thisOption = this.option; var dataArr = thisOption.data || []; var axisType = thisOption.axisType; var names = this._names = []; if (axisType === 'category') { var idxArr = []; each$1(dataArr, function (item, index) { var value = getDataItemValue(item); var newItem; if (isObject$1(item)) { newItem = clone(item); newItem.value = index; } else { newItem = index; } idxArr.push(newItem); if (!isString(value) && (value == null || isNaN(value))) { value = ''; } names.push(value + ''); }); dataArr = idxArr; } var dimType = ({category: 'ordinal', time: 'time'})[axisType] || 'number'; var data = this._data = new List([{name: 'value', type: dimType}], this); data.initData(dataArr, names); }, getData: function () { return this._data; }, /** * @public * @return {Array.<string>} categoreis */ getCategories: function () { if (this.get('axisType') === 'category') { return this._names.slice(); } } }); var SliderTimelineModel = TimelineModel.extend({ type: 'timeline.slider', /** * @protected */ defaultOption: { backgroundColor: 'rgba(0,0,0,0)', // 时间轴背景颜色 borderColor: '#ccc', // 时间轴边框颜色 borderWidth: 0, // 时间轴边框线宽,单位px,默认为0(无边框) orient: 'horizontal', // 'vertical' inverse: false, tooltip: { // boolean or Object trigger: 'item' // data item may also have tootip attr. }, symbol: 'emptyCircle', symbolSize: 10, lineStyle: { show: true, width: 2, color: '#304654' }, label: { // 文本标签 position: 'auto', // auto left right top bottom // When using number, label position is not // restricted by viewRect. // positive: right/bottom, negative: left/top show: true, interval: 'auto', rotate: 0, // formatter: null, // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: '#304654' }, itemStyle: { color: '#304654', borderWidth: 1 }, checkpointStyle: { symbol: 'circle', symbolSize: 13, color: '#c23531', borderWidth: 5, borderColor: 'rgba(194,53,49, 0.5)', animation: true, animationDuration: 300, animationEasing: 'quinticInOut' }, controlStyle: { show: true, showPlayBtn: true, showPrevBtn: true, showNextBtn: true, itemSize: 22, itemGap: 12, position: 'left', // 'left' 'right' 'top' 'bottom' playIcon: 'path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z', // jshint ignore:line stopIcon: 'path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z', // jshint ignore:line nextIcon: 'path://M18.6,50.8l22.5-22.5c0.2-0.2,0.3-0.4,0.3-0.7c0-0.3-0.1-0.5-0.3-0.7L18.7,4.4c-0.1-0.1-0.2-0.3-0.2-0.5 c0-0.4,0.3-0.8,0.8-0.8c0.2,0,0.5,0.1,0.6,0.3l23.5,23.5l0,0c0.2,0.2,0.3,0.4,0.3,0.7c0,0.3-0.1,0.5-0.3,0.7l-0.1,0.1L19.7,52 c-0.1,0.1-0.3,0.2-0.5,0.2c-0.4,0-0.8-0.3-0.8-0.8C18.4,51.2,18.5,51,18.6,50.8z', // jshint ignore:line prevIcon: 'path://M43,52.8L20.4,30.3c-0.2-0.2-0.3-0.4-0.3-0.7c0-0.3,0.1-0.5,0.3-0.7L42.9,6.4c0.1-0.1,0.2-0.3,0.2-0.5 c0-0.4-0.3-0.8-0.8-0.8c-0.2,0-0.5,0.1-0.6,0.3L18.3,28.8l0,0c-0.2,0.2-0.3,0.4-0.3,0.7c0,0.3,0.1,0.5,0.3,0.7l0.1,0.1L41.9,54 c0.1,0.1,0.3,0.2,0.5,0.2c0.4,0,0.8-0.3,0.8-0.8C43.2,53.2,43.1,53,43,52.8z', // jshint ignore:line color: '#304654', borderColor: '#304654', borderWidth: 1 }, emphasis: { label: { show: true, // 其余属性默认使用全局文本样式,详见TEXTSTYLE color: '#c23531' }, itemStyle: { color: '#c23531' }, controlStyle: { color: '#c23531', borderColor: '#c23531', borderWidth: 2 } }, data: [] } }); mixin(SliderTimelineModel, dataFormatMixin); var TimelineView = Component.extend({ type: 'timeline' }); /** * Extend axis 2d * @constructor module:echarts/coord/cartesian/Axis2D * @extends {module:echarts/coord/cartesian/Axis} * @param {string} dim * @param {*} scale * @param {Array.<number>} coordExtent * @param {string} axisType * @param {string} position */ var TimelineAxis = function (dim, scale, coordExtent, axisType) { Axis.call(this, dim, scale, coordExtent); /** * Axis type * - 'category' * - 'value' * - 'time' * - 'log' * @type {string} */ this.type = axisType || 'value'; /** * @private * @type {number} */ this._autoLabelInterval; /** * Axis model * @param {module:echarts/component/TimelineModel} */ this.model = null; }; TimelineAxis.prototype = { constructor: TimelineAxis, /** * @public * @return {number} */ getLabelInterval: function () { var timelineModel = this.model; var labelModel = timelineModel.getModel('label'); var labelInterval = labelModel.get('interval'); if (labelInterval != null && labelInterval != 'auto') { return labelInterval; } var labelInterval = this._autoLabelInterval; if (!labelInterval) { labelInterval = this._autoLabelInterval = getAxisLabelInterval( map(this.scale.getTicks(), this.dataToCoord, this), getFormattedLabels(this, labelModel.get('formatter')), labelModel.getFont(), timelineModel.get('orient') === 'horizontal' ? 0 : 90, labelModel.get('rotate') ); } return labelInterval; }, /** * If label is ignored. * Automatically used when axis is category and label can not be all shown * @public * @param {number} idx * @return {boolean} */ isLabelIgnored: function (idx) { if (this.type === 'category') { var labelInterval = this.getLabelInterval(); return ((typeof labelInterval === 'function') && !labelInterval(idx, this.scale.getLabel(idx))) || idx % (labelInterval + 1); } } }; inherits(TimelineAxis, Axis); var bind$6 = bind; var each$28 = each$1; var PI$4 = Math.PI; TimelineView.extend({ type: 'timeline.slider', init: function (ecModel, api) { this.api = api; /** * @private * @type {module:echarts/component/timeline/TimelineAxis} */ this._axis; /** * @private * @type {module:zrender/core/BoundingRect} */ this._viewRect; /** * @type {number} */ this._timer; /** * @type {module:zrender/Element} */ this._currentPointer; /** * @type {module:zrender/container/Group} */ this._mainGroup; /** * @type {module:zrender/container/Group} */ this._labelGroup; }, /** * @override */ render: function (timelineModel, ecModel, api, payload) { this.model = timelineModel; this.api = api; this.ecModel = ecModel; this.group.removeAll(); if (timelineModel.get('show', true)) { var layoutInfo = this._layout(timelineModel, api); var mainGroup = this._createGroup('mainGroup'); var labelGroup = this._createGroup('labelGroup'); /** * @private * @type {module:echarts/component/timeline/TimelineAxis} */ var axis = this._axis = this._createAxis(layoutInfo, timelineModel); timelineModel.formatTooltip = function (dataIndex) { return encodeHTML(axis.scale.getLabel(dataIndex)); }; each$28( ['AxisLine', 'AxisTick', 'Control', 'CurrentPointer'], function (name) { this['_render' + name](layoutInfo, mainGroup, axis, timelineModel); }, this ); this._renderAxisLabel(layoutInfo, labelGroup, axis, timelineModel); this._position(layoutInfo, timelineModel); } this._doPlayStop(); }, /** * @override */ remove: function () { this._clearTimer(); this.group.removeAll(); }, /** * @override */ dispose: function () { this._clearTimer(); }, _layout: function (timelineModel, api) { var labelPosOpt = timelineModel.get('label.position'); var orient = timelineModel.get('orient'); var viewRect = getViewRect$4(timelineModel, api); // Auto label offset. if (labelPosOpt == null || labelPosOpt === 'auto') { labelPosOpt = orient === 'horizontal' ? ((viewRect.y + viewRect.height / 2) < api.getHeight() / 2 ? '-' : '+') : ((viewRect.x + viewRect.width / 2) < api.getWidth() / 2 ? '+' : '-'); } else if (isNaN(labelPosOpt)) { labelPosOpt = ({ horizontal: {top: '-', bottom: '+'}, vertical: {left: '-', right: '+'} })[orient][labelPosOpt]; } var labelAlignMap = { horizontal: 'center', vertical: (labelPosOpt >= 0 || labelPosOpt === '+') ? 'left' : 'right' }; var labelBaselineMap = { horizontal: (labelPosOpt >= 0 || labelPosOpt === '+') ? 'top' : 'bottom', vertical: 'middle' }; var rotationMap = { horizontal: 0, vertical: PI$4 / 2 }; // Position var mainLength = orient === 'vertical' ? viewRect.height : viewRect.width; var controlModel = timelineModel.getModel('controlStyle'); var showControl = controlModel.get('show', true); var controlSize = showControl ? controlModel.get('itemSize') : 0; var controlGap = showControl ? controlModel.get('itemGap') : 0; var sizePlusGap = controlSize + controlGap; // Special label rotate. var labelRotation = timelineModel.get('label.rotate') || 0; labelRotation = labelRotation * PI$4 / 180; // To radian. var playPosition; var prevBtnPosition; var nextBtnPosition; var axisExtent; var controlPosition = controlModel.get('position', true); var showPlayBtn = showControl && controlModel.get('showPlayBtn', true); var showPrevBtn = showControl && controlModel.get('showPrevBtn', true); var showNextBtn = showControl && controlModel.get('showNextBtn', true); var xLeft = 0; var xRight = mainLength; // position[0] means left, position[1] means middle. if (controlPosition === 'left' || controlPosition === 'bottom') { showPlayBtn && (playPosition = [0, 0], xLeft += sizePlusGap); showPrevBtn && (prevBtnPosition = [xLeft, 0], xLeft += sizePlusGap); showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); } else { // 'top' 'right' showPlayBtn && (playPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); showPrevBtn && (prevBtnPosition = [0, 0], xLeft += sizePlusGap); showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); } axisExtent = [xLeft, xRight]; if (timelineModel.get('inverse')) { axisExtent.reverse(); } return { viewRect: viewRect, mainLength: mainLength, orient: orient, rotation: rotationMap[orient], labelRotation: labelRotation, labelPosOpt: labelPosOpt, labelAlign: timelineModel.get('label.align') || labelAlignMap[orient], labelBaseline: timelineModel.get('label.verticalAlign') || timelineModel.get('label.baseline') || labelBaselineMap[orient], // Based on mainGroup. playPosition: playPosition, prevBtnPosition: prevBtnPosition, nextBtnPosition: nextBtnPosition, axisExtent: axisExtent, controlSize: controlSize, controlGap: controlGap }; }, _position: function (layoutInfo, timelineModel) { // Position is be called finally, because bounding rect is needed for // adapt content to fill viewRect (auto adapt offset). // Timeline may be not all in the viewRect when 'offset' is specified // as a number, because it is more appropriate that label aligns at // 'offset' but not the other edge defined by viewRect. var mainGroup = this._mainGroup; var labelGroup = this._labelGroup; var viewRect = layoutInfo.viewRect; if (layoutInfo.orient === 'vertical') { // transform to horizontal, inverse rotate by left-top point. var m = create$1(); var rotateOriginX = viewRect.x; var rotateOriginY = viewRect.y + viewRect.height; translate(m, m, [-rotateOriginX, -rotateOriginY]); rotate(m, m, -PI$4 / 2); translate(m, m, [rotateOriginX, rotateOriginY]); viewRect = viewRect.clone(); viewRect.applyTransform(m); } var viewBound = getBound(viewRect); var mainBound = getBound(mainGroup.getBoundingRect()); var labelBound = getBound(labelGroup.getBoundingRect()); var mainPosition = mainGroup.position; var labelsPosition = labelGroup.position; labelsPosition[0] = mainPosition[0] = viewBound[0][0]; var labelPosOpt = layoutInfo.labelPosOpt; if (isNaN(labelPosOpt)) { // '+' or '-' var mainBoundIdx = labelPosOpt === '+' ? 0 : 1; toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx); toBound(labelsPosition, labelBound, viewBound, 1, 1 - mainBoundIdx); } else { var mainBoundIdx = labelPosOpt >= 0 ? 0 : 1; toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx); labelsPosition[1] = mainPosition[1] + labelPosOpt; } mainGroup.attr('position', mainPosition); labelGroup.attr('position', labelsPosition); mainGroup.rotation = labelGroup.rotation = layoutInfo.rotation; setOrigin(mainGroup); setOrigin(labelGroup); function setOrigin(targetGroup) { var pos = targetGroup.position; targetGroup.origin = [ viewBound[0][0] - pos[0], viewBound[1][0] - pos[1] ]; } function getBound(rect) { // [[xmin, xmax], [ymin, ymax]] return [ [rect.x, rect.x + rect.width], [rect.y, rect.y + rect.height] ]; } function toBound(fromPos, from, to, dimIdx, boundIdx) { fromPos[dimIdx] += to[dimIdx][boundIdx] - from[dimIdx][boundIdx]; } }, _createAxis: function (layoutInfo, timelineModel) { var data = timelineModel.getData(); var axisType = timelineModel.get('axisType'); var scale = createScaleByModel(timelineModel, axisType); var dataExtent = data.getDataExtent('value'); scale.setExtent(dataExtent[0], dataExtent[1]); this._customizeScale(scale, data); scale.niceTicks(); var axis = new TimelineAxis('value', scale, layoutInfo.axisExtent, axisType); axis.model = timelineModel; return axis; }, _customizeScale: function (scale, data) { scale.getTicks = function () { return data.mapArray(['value'], function (value) { return value; }); }; scale.getTicksLabels = function () { return map(this.getTicks(), scale.getLabel, scale); }; }, _createGroup: function (name) { var newGroup = this['_' + name] = new Group(); this.group.add(newGroup); return newGroup; }, _renderAxisLine: function (layoutInfo, group, axis, timelineModel) { var axisExtent = axis.getExtent(); if (!timelineModel.get('lineStyle.show')) { return; } group.add(new Line({ shape: { x1: axisExtent[0], y1: 0, x2: axisExtent[1], y2: 0 }, style: extend( {lineCap: 'round'}, timelineModel.getModel('lineStyle').getLineStyle() ), silent: true, z2: 1 })); }, /** * @private */ _renderAxisTick: function (layoutInfo, group, axis, timelineModel) { var data = timelineModel.getData(); var ticks = axis.scale.getTicks(); each$28(ticks, function (value, dataIndex) { var tickCoord = axis.dataToCoord(value); var itemModel = data.getItemModel(dataIndex); var itemStyleModel = itemModel.getModel('itemStyle'); var hoverStyleModel = itemModel.getModel('emphasis.itemStyle'); var symbolOpt = { position: [tickCoord, 0], onclick: bind$6(this._changeTimeline, this, dataIndex) }; var el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt); setHoverStyle(el, hoverStyleModel.getItemStyle()); if (itemModel.get('tooltip')) { el.dataIndex = dataIndex; el.dataModel = timelineModel; } else { el.dataIndex = el.dataModel = null; } }, this); }, /** * @private */ _renderAxisLabel: function (layoutInfo, group, axis, timelineModel) { var labelModel = timelineModel.getModel('label'); if (!labelModel.get('show')) { return; } var data = timelineModel.getData(); var ticks = axis.scale.getTicks(); var labels = getFormattedLabels( axis, labelModel.get('formatter') ); var labelInterval = axis.getLabelInterval(); each$28(ticks, function (tick, dataIndex) { if (axis.isLabelIgnored(dataIndex, labelInterval)) { return; } var itemModel = data.getItemModel(dataIndex); var normalLabelModel = itemModel.getModel('label'); var hoverLabelModel = itemModel.getModel('emphasis.label'); var tickCoord = axis.dataToCoord(tick); var textEl = new Text({ position: [tickCoord, 0], rotation: layoutInfo.labelRotation - layoutInfo.rotation, onclick: bind$6(this._changeTimeline, this, dataIndex), silent: false }); setTextStyle(textEl.style, normalLabelModel, { text: labels[dataIndex], textAlign: layoutInfo.labelAlign, textVerticalAlign: layoutInfo.labelBaseline }); group.add(textEl); setHoverStyle( textEl, setTextStyle({}, hoverLabelModel) ); }, this); }, /** * @private */ _renderControl: function (layoutInfo, group, axis, timelineModel) { var controlSize = layoutInfo.controlSize; var rotation = layoutInfo.rotation; var itemStyle = timelineModel.getModel('controlStyle').getItemStyle(); var hoverStyle = timelineModel.getModel('emphasis.controlStyle').getItemStyle(); var rect = [0, -controlSize / 2, controlSize, controlSize]; var playState = timelineModel.getPlayState(); var inverse = timelineModel.get('inverse', true); makeBtn( layoutInfo.nextBtnPosition, 'controlStyle.nextIcon', bind$6(this._changeTimeline, this, inverse ? '-' : '+') ); makeBtn( layoutInfo.prevBtnPosition, 'controlStyle.prevIcon', bind$6(this._changeTimeline, this, inverse ? '+' : '-') ); makeBtn( layoutInfo.playPosition, 'controlStyle.' + (playState ? 'stopIcon' : 'playIcon'), bind$6(this._handlePlayClick, this, !playState), true ); function makeBtn(position, iconPath, onclick, willRotate) { if (!position) { return; } var opt = { position: position, origin: [controlSize / 2, 0], rotation: willRotate ? -rotation : 0, rectHover: true, style: itemStyle, onclick: onclick }; var btn = makeIcon(timelineModel, iconPath, rect, opt); group.add(btn); setHoverStyle(btn, hoverStyle); } }, _renderCurrentPointer: function (layoutInfo, group, axis, timelineModel) { var data = timelineModel.getData(); var currentIndex = timelineModel.getCurrentIndex(); var pointerModel = data.getItemModel(currentIndex).getModel('checkpointStyle'); var me = this; var callback = { onCreate: function (pointer) { pointer.draggable = true; pointer.drift = bind$6(me._handlePointerDrag, me); pointer.ondragend = bind$6(me._handlePointerDragend, me); pointerMoveTo(pointer, currentIndex, axis, timelineModel, true); }, onUpdate: function (pointer) { pointerMoveTo(pointer, currentIndex, axis, timelineModel); } }; // Reuse when exists, for animation and drag. this._currentPointer = giveSymbol( pointerModel, pointerModel, this._mainGroup, {}, this._currentPointer, callback ); }, _handlePlayClick: function (nextState) { this._clearTimer(); this.api.dispatchAction({ type: 'timelinePlayChange', playState: nextState, from: this.uid }); }, _handlePointerDrag: function (dx, dy, e) { this._clearTimer(); this._pointerChangeTimeline([e.offsetX, e.offsetY]); }, _handlePointerDragend: function (e) { this._pointerChangeTimeline([e.offsetX, e.offsetY], true); }, _pointerChangeTimeline: function (mousePos, trigger) { var toCoord = this._toAxisCoord(mousePos)[0]; var axis = this._axis; var axisExtent = asc(axis.getExtent().slice()); toCoord > axisExtent[1] && (toCoord = axisExtent[1]); toCoord < axisExtent[0] && (toCoord = axisExtent[0]); this._currentPointer.position[0] = toCoord; this._currentPointer.dirty(); var targetDataIndex = this._findNearestTick(toCoord); var timelineModel = this.model; if (trigger || ( targetDataIndex !== timelineModel.getCurrentIndex() && timelineModel.get('realtime') )) { this._changeTimeline(targetDataIndex); } }, _doPlayStop: function () { this._clearTimer(); if (this.model.getPlayState()) { this._timer = setTimeout( bind$6(handleFrame, this), this.model.get('playInterval') ); } function handleFrame() { // Do not cache var timelineModel = this.model; this._changeTimeline( timelineModel.getCurrentIndex() + (timelineModel.get('rewind', true) ? -1 : 1) ); } }, _toAxisCoord: function (vertex) { var trans = this._mainGroup.getLocalTransform(); return applyTransform$1(vertex, trans, true); }, _findNearestTick: function (axisCoord) { var data = this.model.getData(); var dist = Infinity; var targetDataIndex; var axis = this._axis; data.each(['value'], function (value, dataIndex) { var coord = axis.dataToCoord(value); var d = Math.abs(coord - axisCoord); if (d < dist) { dist = d; targetDataIndex = dataIndex; } }); return targetDataIndex; }, _clearTimer: function () { if (this._timer) { clearTimeout(this._timer); this._timer = null; } }, _changeTimeline: function (nextIndex) { var currentIndex = this.model.getCurrentIndex(); if (nextIndex === '+') { nextIndex = currentIndex + 1; } else if (nextIndex === '-') { nextIndex = currentIndex - 1; } this.api.dispatchAction({ type: 'timelineChange', currentIndex: nextIndex, from: this.uid }); } }); function getViewRect$4(model, api) { return getLayoutRect( model.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() }, model.get('padding') ); } function makeIcon(timelineModel, objPath, rect, opts) { var icon = makePath( timelineModel.get(objPath).replace(/^path:\/\//, ''), clone(opts || {}), new BoundingRect(rect[0], rect[1], rect[2], rect[3]), 'center' ); return icon; } /** * Create symbol or update symbol * opt: basic position and event handlers */ function giveSymbol(hostModel, itemStyleModel, group, opt, symbol, callback) { var color = itemStyleModel.get('color'); if (!symbol) { var symbolType = hostModel.get('symbol'); symbol = createSymbol(symbolType, -1, -1, 2, 2, color); symbol.setStyle('strokeNoScale', true); group.add(symbol); callback && callback.onCreate(symbol); } else { symbol.setColor(color); group.add(symbol); // Group may be new, also need to add. callback && callback.onUpdate(symbol); } // Style var itemStyle = itemStyleModel.getItemStyle(['color', 'symbol', 'symbolSize']); symbol.setStyle(itemStyle); // Transform and events. opt = merge({ rectHover: true, z2: 100 }, opt, true); var symbolSize = hostModel.get('symbolSize'); symbolSize = symbolSize instanceof Array ? symbolSize.slice() : [+symbolSize, +symbolSize]; symbolSize[0] /= 2; symbolSize[1] /= 2; opt.scale = symbolSize; var symbolOffset = hostModel.get('symbolOffset'); if (symbolOffset) { var pos = opt.position = opt.position || [0, 0]; pos[0] += parsePercent$1(symbolOffset[0], symbolSize[0]); pos[1] += parsePercent$1(symbolOffset[1], symbolSize[1]); } var symbolRotate = hostModel.get('symbolRotate'); opt.rotation = (symbolRotate || 0) * Math.PI / 180 || 0; symbol.attr(opt); // FIXME // (1) When symbol.style.strokeNoScale is true and updateTransform is not performed, // getBoundingRect will return wrong result. // (This is supposed to be resolved in zrender, but it is a little difficult to // leverage performance and auto updateTransform) // (2) All of ancesters of symbol do not scale, so we can just updateTransform symbol. symbol.updateTransform(); return symbol; } function pointerMoveTo(pointer, dataIndex, axis, timelineModel, noAnimation) { if (pointer.dragging) { return; } var pointerModel = timelineModel.getModel('checkpointStyle'); var toCoord = axis.dataToCoord(timelineModel.getData().get(['value'], dataIndex)); if (noAnimation || !pointerModel.get('animation', true)) { pointer.attr({position: [toCoord, 0]}); } else { pointer.stopAnimation(true); pointer.animateTo( {position: [toCoord, 0]}, pointerModel.get('animationDuration', true), pointerModel.get('animationEasing', true) ); } } /** * DataZoom component entry */ registerPreprocessor(preprocessor$3); var ToolboxModel = extendComponentModel({ type: 'toolbox', layoutMode: { type: 'box', ignoreSize: true }, mergeDefaultAndTheme: function (option) { ToolboxModel.superApply(this, 'mergeDefaultAndTheme', arguments); each$1(this.option.feature, function (featureOpt, featureName) { var Feature = get$1(featureName); Feature && merge(featureOpt, Feature.defaultOption); }); }, defaultOption: { show: true, z: 6, zlevel: 0, orient: 'horizontal', left: 'right', top: 'top', // right // bottom backgroundColor: 'transparent', borderColor: '#ccc', borderRadius: 0, borderWidth: 0, padding: 5, itemSize: 15, itemGap: 8, showTitle: true, iconStyle: { borderColor: '#666', color: 'none' }, emphasis: { iconStyle: { borderColor: '#3E98C5' } } // textStyle: {}, // feature } }); extendComponentView({ type: 'toolbox', render: function (toolboxModel, ecModel, api, payload) { var group = this.group; group.removeAll(); if (!toolboxModel.get('show')) { return; } var itemSize = +toolboxModel.get('itemSize'); var featureOpts = toolboxModel.get('feature') || {}; var features = this._features || (this._features = {}); var featureNames = []; each$1(featureOpts, function (opt, name) { featureNames.push(name); }); (new DataDiffer(this._featureNames || [], featureNames)) .add(processFeature) .update(processFeature) .remove(curry(processFeature, null)) .execute(); // Keep for diff. this._featureNames = featureNames; function processFeature(newIndex, oldIndex) { var featureName = featureNames[newIndex]; var oldName = featureNames[oldIndex]; var featureOpt = featureOpts[featureName]; var featureModel = new Model(featureOpt, toolboxModel, toolboxModel.ecModel); var feature; if (featureName && !oldName) { // Create if (isUserFeatureName(featureName)) { feature = { model: featureModel, onclick: featureModel.option.onclick, featureName: featureName }; } else { var Feature = get$1(featureName); if (!Feature) { return; } feature = new Feature(featureModel, ecModel, api); } features[featureName] = feature; } else { feature = features[oldName]; // If feature does not exsit. if (!feature) { return; } feature.model = featureModel; feature.ecModel = ecModel; feature.api = api; } if (!featureName && oldName) { feature.dispose && feature.dispose(ecModel, api); return; } if (!featureModel.get('show') || feature.unusable) { feature.remove && feature.remove(ecModel, api); return; } createIconPaths(featureModel, feature, featureName); featureModel.setIconStatus = function (iconName, status) { var option = this.option; var iconPaths = this.iconPaths; option.iconStatus = option.iconStatus || {}; option.iconStatus[iconName] = status; // FIXME iconPaths[iconName] && iconPaths[iconName].trigger(status); }; if (feature.render) { feature.render(featureModel, ecModel, api, payload); } } function createIconPaths(featureModel, feature, featureName) { var iconStyleModel = featureModel.getModel('iconStyle'); var iconStyleEmphasisModel = featureModel.getModel('emphasis.iconStyle'); // If one feature has mutiple icon. they are orginaized as // { // icon: { // foo: '', // bar: '' // }, // title: { // foo: '', // bar: '' // } // } var icons = feature.getIcons ? feature.getIcons() : featureModel.get('icon'); var titles = featureModel.get('title') || {}; if (typeof icons === 'string') { var icon = icons; var title = titles; icons = {}; titles = {}; icons[featureName] = icon; titles[featureName] = title; } var iconPaths = featureModel.iconPaths = {}; each$1(icons, function (iconStr, iconName) { var path = createIcon( iconStr, {}, { x: -itemSize / 2, y: -itemSize / 2, width: itemSize, height: itemSize } ); path.setStyle(iconStyleModel.getItemStyle()); path.hoverStyle = iconStyleEmphasisModel.getItemStyle(); setHoverStyle(path); if (toolboxModel.get('showTitle')) { path.__title = titles[iconName]; path.on('mouseover', function () { // Should not reuse above hoverStyle, which might be modified. var hoverStyle = iconStyleEmphasisModel.getItemStyle(); path.setStyle({ text: titles[iconName], textPosition: hoverStyle.textPosition || 'bottom', textFill: hoverStyle.fill || hoverStyle.stroke || '#000', textAlign: hoverStyle.textAlign || 'center' }); }) .on('mouseout', function () { path.setStyle({ textFill: null }); }); } path.trigger(featureModel.get('iconStatus.' + iconName) || 'normal'); group.add(path); path.on('click', bind( feature.onclick, feature, ecModel, api, iconName )); iconPaths[iconName] = path; }); } layout$3(group, toolboxModel, api); // Render background after group is layout // FIXME group.add(makeBackground(group.getBoundingRect(), toolboxModel)); // Adjust icon title positions to avoid them out of screen group.eachChild(function (icon) { var titleText = icon.__title; var hoverStyle = icon.hoverStyle; // May be background element if (hoverStyle && titleText) { var rect = getBoundingRect( titleText, makeFont(hoverStyle) ); var offsetX = icon.position[0] + group.position[0]; var offsetY = icon.position[1] + group.position[1] + itemSize; var needPutOnTop = false; if (offsetY + rect.height > api.getHeight()) { hoverStyle.textPosition = 'top'; needPutOnTop = true; } var topOffset = needPutOnTop ? (-5 - rect.height) : (itemSize + 8); if (offsetX + rect.width / 2 > api.getWidth()) { hoverStyle.textPosition = ['100%', topOffset]; hoverStyle.textAlign = 'right'; } else if (offsetX - rect.width / 2 < 0) { hoverStyle.textPosition = [0, topOffset]; hoverStyle.textAlign = 'left'; } } }); }, updateView: function (toolboxModel, ecModel, api, payload) { each$1(this._features, function (feature) { feature.updateView && feature.updateView(feature.model, ecModel, api, payload); }); }, // updateLayout: function (toolboxModel, ecModel, api, payload) { // zrUtil.each(this._features, function (feature) { // feature.updateLayout && feature.updateLayout(feature.model, ecModel, api, payload); // }); // }, remove: function (ecModel, api) { each$1(this._features, function (feature) { feature.remove && feature.remove(ecModel, api); }); this.group.removeAll(); }, dispose: function (ecModel, api) { each$1(this._features, function (feature) { feature.dispose && feature.dispose(ecModel, api); }); } }); function isUserFeatureName(featureName) { return featureName.indexOf('my') === 0; } var saveAsImageLang = lang.toolbox.saveAsImage; function SaveAsImage(model) { this.model = model; } SaveAsImage.defaultOption = { show: true, icon: 'M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0', title: saveAsImageLang.title, type: 'png', // Default use option.backgroundColor // backgroundColor: '#fff', name: '', excludeComponents: ['toolbox'], pixelRatio: 1, lang: saveAsImageLang.lang.slice() }; SaveAsImage.prototype.unusable = !env$1.canvasSupported; var proto$4 = SaveAsImage.prototype; proto$4.onclick = function (ecModel, api) { var model = this.model; var title = model.get('name') || ecModel.get('title.0.text') || 'echarts'; var $a = document.createElement('a'); var type = model.get('type', true) || 'png'; $a.download = title + '.' + type; $a.target = '_blank'; var url = api.getConnectedDataURL({ type: type, backgroundColor: model.get('backgroundColor', true) || ecModel.get('backgroundColor') || '#fff', excludeComponents: model.get('excludeComponents'), pixelRatio: model.get('pixelRatio') }); $a.href = url; // Chrome and Firefox if (typeof MouseEvent === 'function' && !env$1.browser.ie && !env$1.browser.edge) { var evt = new MouseEvent('click', { view: window, bubbles: true, cancelable: false }); $a.dispatchEvent(evt); } // IE else { if (window.navigator.msSaveOrOpenBlob) { var bstr = atob(url.split(',')[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var blob = new Blob([u8arr]); window.navigator.msSaveOrOpenBlob(blob, title + '.' + type); } else { var lang$$1 = model.get('lang'); var html = '' + '<body style="margin:0;">' + '<img src="' + url + '" style="max-width:100%;" title="' + ((lang$$1 && lang$$1[0]) || '') + '" />' + '</body>'; var tab = window.open(); tab.document.write(html); } } }; register$1( 'saveAsImage', SaveAsImage ); var magicTypeLang = lang.toolbox.magicType; function MagicType(model) { this.model = model; } MagicType.defaultOption = { show: true, type: [], // Icon group icon: { line: 'M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4', bar: 'M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7', stack: 'M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z', // jshint ignore:line tiled: 'M2.3,2.2h22.8V25H2.3V2.2z M35,2.2h22.8V25H35V2.2zM2.3,35h22.8v22.8H2.3V35z M35,35h22.8v22.8H35V35z' }, // `line`, `bar`, `stack`, `tiled` title: clone(magicTypeLang.title), option: {}, seriesIndex: {} }; var proto$5 = MagicType.prototype; proto$5.getIcons = function () { var model = this.model; var availableIcons = model.get('icon'); var icons = {}; each$1(model.get('type'), function (type) { if (availableIcons[type]) { icons[type] = availableIcons[type]; } }); return icons; }; var seriesOptGenreator = { 'line': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'bar') { return merge({ id: seriesId, type: 'line', // Preserve data related option data: seriesModel.get('data'), stack: seriesModel.get('stack'), markPoint: seriesModel.get('markPoint'), markLine: seriesModel.get('markLine') }, model.get('option.line') || {}, true); } }, 'bar': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'line') { return merge({ id: seriesId, type: 'bar', // Preserve data related option data: seriesModel.get('data'), stack: seriesModel.get('stack'), markPoint: seriesModel.get('markPoint'), markLine: seriesModel.get('markLine') }, model.get('option.bar') || {}, true); } }, 'stack': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'line' || seriesType === 'bar') { return merge({ id: seriesId, stack: '__ec_magicType_stack__' }, model.get('option.stack') || {}, true); } }, 'tiled': function (seriesType, seriesId, seriesModel, model) { if (seriesType === 'line' || seriesType === 'bar') { return merge({ id: seriesId, stack: '' }, model.get('option.tiled') || {}, true); } } }; var radioTypes = [ ['line', 'bar'], ['stack', 'tiled'] ]; proto$5.onclick = function (ecModel, api, type) { var model = this.model; var seriesIndex = model.get('seriesIndex.' + type); // Not supported magicType if (!seriesOptGenreator[type]) { return; } var newOption = { series: [] }; var generateNewSeriesTypes = function (seriesModel) { var seriesType = seriesModel.subType; var seriesId = seriesModel.id; var newSeriesOpt = seriesOptGenreator[type]( seriesType, seriesId, seriesModel, model ); if (newSeriesOpt) { // PENDING If merge original option? defaults(newSeriesOpt, seriesModel.option); newOption.series.push(newSeriesOpt); } // Modify boundaryGap var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.type === 'cartesian2d' && (type === 'line' || type === 'bar')) { var categoryAxis = coordSys.getAxesByScale('ordinal')[0]; if (categoryAxis) { var axisDim = categoryAxis.dim; var axisType = axisDim + 'Axis'; var axisModel = ecModel.queryComponents({ mainType: axisType, index: seriesModel.get(name + 'Index'), id: seriesModel.get(name + 'Id') })[0]; var axisIndex = axisModel.componentIndex; newOption[axisType] = newOption[axisType] || []; for (var i = 0; i <= axisIndex; i++) { newOption[axisType][axisIndex] = newOption[axisType][axisIndex] || {}; } newOption[axisType][axisIndex].boundaryGap = type === 'bar' ? true : false; } } }; each$1(radioTypes, function (radio) { if (indexOf(radio, type) >= 0) { each$1(radio, function (item) { model.setIconStatus(item, 'normal'); }); } }); model.setIconStatus(type, 'emphasis'); ecModel.eachComponent( { mainType: 'series', query: seriesIndex == null ? null : { seriesIndex: seriesIndex } }, generateNewSeriesTypes ); api.dispatchAction({ type: 'changeMagicType', currentType: type, newOption: newOption }); }; registerAction({ type: 'changeMagicType', event: 'magicTypeChanged', update: 'prepareAndUpdate' }, function (payload, ecModel) { ecModel.mergeOption(payload.newOption); }); register$1('magicType', MagicType); var dataViewLang = lang.toolbox.dataView; var BLOCK_SPLITER = new Array(60).join('-'); var ITEM_SPLITER = '\t'; /** * Group series into two types * 1. on category axis, like line, bar * 2. others, like scatter, pie * @param {module:echarts/model/Global} ecModel * @return {Object} * @inner */ function groupSeries(ecModel) { var seriesGroupByCategoryAxis = {}; var otherSeries = []; var meta = []; ecModel.eachRawSeries(function (seriesModel) { var coordSys = seriesModel.coordinateSystem; if (coordSys && (coordSys.type === 'cartesian2d' || coordSys.type === 'polar')) { var baseAxis = coordSys.getBaseAxis(); if (baseAxis.type === 'category') { var key = baseAxis.dim + '_' + baseAxis.index; if (!seriesGroupByCategoryAxis[key]) { seriesGroupByCategoryAxis[key] = { categoryAxis: baseAxis, valueAxis: coordSys.getOtherAxis(baseAxis), series: [] }; meta.push({ axisDim: baseAxis.dim, axisIndex: baseAxis.index }); } seriesGroupByCategoryAxis[key].series.push(seriesModel); } else { otherSeries.push(seriesModel); } } else { otherSeries.push(seriesModel); } }); return { seriesGroupByCategoryAxis: seriesGroupByCategoryAxis, other: otherSeries, meta: meta }; } /** * Assemble content of series on cateogory axis * @param {Array.<module:echarts/model/Series>} series * @return {string} * @inner */ function assembleSeriesWithCategoryAxis(series) { var tables = []; each$1(series, function (group, key) { var categoryAxis = group.categoryAxis; var valueAxis = group.valueAxis; var valueAxisDim = valueAxis.dim; var headers = [' '].concat(map(group.series, function (series) { return series.name; })); var columns = [categoryAxis.model.getCategories()]; each$1(group.series, function (series) { columns.push(series.getRawData().mapArray(valueAxisDim, function (val) { return val; })); }); // Assemble table content var lines = [headers.join(ITEM_SPLITER)]; for (var i = 0; i < columns[0].length; i++) { var items = []; for (var j = 0; j < columns.length; j++) { items.push(columns[j][i]); } lines.push(items.join(ITEM_SPLITER)); } tables.push(lines.join('\n')); }); return tables.join('\n\n' + BLOCK_SPLITER + '\n\n'); } /** * Assemble content of other series * @param {Array.<module:echarts/model/Series>} series * @return {string} * @inner */ function assembleOtherSeries(series) { return map(series, function (series) { var data = series.getRawData(); var lines = [series.name]; var vals = []; data.each(data.dimensions, function () { var argLen = arguments.length; var dataIndex = arguments[argLen - 1]; var name = data.getName(dataIndex); for (var i = 0; i < argLen - 1; i++) { vals[i] = arguments[i]; } lines.push((name ? (name + ITEM_SPLITER) : '') + vals.join(ITEM_SPLITER)); }); return lines.join('\n'); }).join('\n\n' + BLOCK_SPLITER + '\n\n'); } /** * @param {module:echarts/model/Global} * @return {Object} * @inner */ function getContentFromModel(ecModel) { var result = groupSeries(ecModel); return { value: filter([ assembleSeriesWithCategoryAxis(result.seriesGroupByCategoryAxis), assembleOtherSeries(result.other) ], function (str) { return str.replace(/[\n\t\s]/g, ''); }).join('\n\n' + BLOCK_SPLITER + '\n\n'), meta: result.meta }; } function trim$1(str) { return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } /** * If a block is tsv format */ function isTSVFormat(block) { // Simple method to find out if a block is tsv format var firstLine = block.slice(0, block.indexOf('\n')); if (firstLine.indexOf(ITEM_SPLITER) >= 0) { return true; } } var itemSplitRegex = new RegExp('[' + ITEM_SPLITER + ']+', 'g'); /** * @param {string} tsv * @return {Object} */ function parseTSVContents(tsv) { var tsvLines = tsv.split(/\n+/g); var headers = trim$1(tsvLines.shift()).split(itemSplitRegex); var categories = []; var series = map(headers, function (header) { return { name: header, data: [] }; }); for (var i = 0; i < tsvLines.length; i++) { var items = trim$1(tsvLines[i]).split(itemSplitRegex); categories.push(items.shift()); for (var j = 0; j < items.length; j++) { series[j] && (series[j].data[i] = items[j]); } } return { series: series, categories: categories }; } /** * @param {string} str * @return {Array.<Object>} * @inner */ function parseListContents(str) { var lines = str.split(/\n+/g); var seriesName = trim$1(lines.shift()); var data = []; for (var i = 0; i < lines.length; i++) { var items = trim$1(lines[i]).split(itemSplitRegex); var name = ''; var value; var hasName = false; if (isNaN(items[0])) { // First item is name hasName = true; name = items[0]; items = items.slice(1); data[i] = { name: name, value: [] }; value = data[i].value; } else { value = data[i] = []; } for (var j = 0; j < items.length; j++) { value.push(+items[j]); } if (value.length === 1) { hasName ? (data[i].value = value[0]) : (data[i] = value[0]); } } return { name: seriesName, data: data }; } /** * @param {string} str * @param {Array.<Object>} blockMetaList * @return {Object} * @inner */ function parseContents(str, blockMetaList) { var blocks = str.split(new RegExp('\n*' + BLOCK_SPLITER + '\n*', 'g')); var newOption = { series: [] }; each$1(blocks, function (block, idx) { if (isTSVFormat(block)) { var result = parseTSVContents(block); var blockMeta = blockMetaList[idx]; var axisKey = blockMeta.axisDim + 'Axis'; if (blockMeta) { newOption[axisKey] = newOption[axisKey] || []; newOption[axisKey][blockMeta.axisIndex] = { data: result.categories }; newOption.series = newOption.series.concat(result.series); } } else { var result = parseListContents(block); newOption.series.push(result); } }); return newOption; } /** * @alias {module:echarts/component/toolbox/feature/DataView} * @constructor * @param {module:echarts/model/Model} model */ function DataView(model) { this._dom = null; this.model = model; } DataView.defaultOption = { show: true, readOnly: false, optionToContent: null, contentToOption: null, icon: 'M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28', title: clone(dataViewLang.title), lang: clone(dataViewLang.lang), backgroundColor: '#fff', textColor: '#000', textareaColor: '#fff', textareaBorderColor: '#333', buttonColor: '#c23531', buttonTextColor: '#fff' }; DataView.prototype.onclick = function (ecModel, api) { var container = api.getDom(); var model = this.model; if (this._dom) { container.removeChild(this._dom); } var root = document.createElement('div'); root.style.cssText = 'position:absolute;left:5px;top:5px;bottom:5px;right:5px;'; root.style.backgroundColor = model.get('backgroundColor') || '#fff'; // Create elements var header = document.createElement('h4'); var lang$$1 = model.get('lang') || []; header.innerHTML = lang$$1[0] || model.get('title'); header.style.cssText = 'margin: 10px 20px;'; header.style.color = model.get('textColor'); var viewMain = document.createElement('div'); var textarea = document.createElement('textarea'); viewMain.style.cssText = 'display:block;width:100%;overflow:auto;'; var optionToContent = model.get('optionToContent'); var contentToOption = model.get('contentToOption'); var result = getContentFromModel(ecModel); if (typeof optionToContent === 'function') { var htmlOrDom = optionToContent(api.getOption()); if (typeof htmlOrDom === 'string') { viewMain.innerHTML = htmlOrDom; } else if (isDom(htmlOrDom)) { viewMain.appendChild(htmlOrDom); } } else { // Use default textarea viewMain.appendChild(textarea); textarea.readOnly = model.get('readOnly'); textarea.style.cssText = 'width:100%;height:100%;font-family:monospace;font-size:14px;line-height:1.6rem;'; textarea.style.color = model.get('textColor'); textarea.style.borderColor = model.get('textareaBorderColor'); textarea.style.backgroundColor = model.get('textareaColor'); textarea.value = result.value; } var blockMetaList = result.meta; var buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'position:absolute;bottom:0;left:0;right:0;'; var buttonStyle = 'float:right;margin-right:20px;border:none;' + 'cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px'; var closeButton = document.createElement('div'); var refreshButton = document.createElement('div'); buttonStyle += ';background-color:' + model.get('buttonColor'); buttonStyle += ';color:' + model.get('buttonTextColor'); var self = this; function close() { container.removeChild(root); self._dom = null; } addEventListener(closeButton, 'click', close); addEventListener(refreshButton, 'click', function () { var newOption; try { if (typeof contentToOption === 'function') { newOption = contentToOption(viewMain, api.getOption()); } else { newOption = parseContents(textarea.value, blockMetaList); } } catch (e) { close(); throw new Error('Data view format error ' + e); } if (newOption) { api.dispatchAction({ type: 'changeDataView', newOption: newOption }); } close(); }); closeButton.innerHTML = lang$$1[1]; refreshButton.innerHTML = lang$$1[2]; refreshButton.style.cssText = buttonStyle; closeButton.style.cssText = buttonStyle; !model.get('readOnly') && buttonContainer.appendChild(refreshButton); buttonContainer.appendChild(closeButton); // http://stackoverflow.com/questions/6637341/use-tab-to-indent-in-textarea addEventListener(textarea, 'keydown', function (e) { if ((e.keyCode || e.which) === 9) { // get caret position/selection var val = this.value; var start = this.selectionStart; var end = this.selectionEnd; // set textarea value to: text before caret + tab + text after caret this.value = val.substring(0, start) + ITEM_SPLITER + val.substring(end); // put caret at right position again this.selectionStart = this.selectionEnd = start + 1; // prevent the focus lose stop(e); } }); root.appendChild(header); root.appendChild(viewMain); root.appendChild(buttonContainer); viewMain.style.height = (container.clientHeight - 80) + 'px'; container.appendChild(root); this._dom = root; }; DataView.prototype.remove = function (ecModel, api) { this._dom && api.getDom().removeChild(this._dom); }; DataView.prototype.dispose = function (ecModel, api) { this.remove(ecModel, api); }; /** * @inner */ function tryMergeDataOption(newData, originalData) { return map(newData, function (newVal, idx) { var original = originalData && originalData[idx]; if (isObject$1(original) && !isArray(original)) { if (isObject$1(newVal) && !isArray(newVal)) { newVal = newVal.value; } // Original data has option return defaults({ value: newVal }, original); } else { return newVal; } }); } register$1('dataView', DataView); registerAction({ type: 'changeDataView', event: 'dataViewChanged', update: 'prepareAndUpdate' }, function (payload, ecModel) { var newSeriesOptList = []; each$1(payload.newOption.series, function (seriesOpt) { var seriesModel = ecModel.getSeriesByName(seriesOpt.name)[0]; if (!seriesModel) { // New created series // Geuss the series type newSeriesOptList.push(extend({ // Default is scatter type: 'scatter' }, seriesOpt)); } else { var originalData = seriesModel.get('data'); newSeriesOptList.push({ name: seriesOpt.name, data: tryMergeDataOption(seriesOpt.data, originalData) }); } }); ecModel.mergeOption(defaults({ series: newSeriesOptList }, payload.newOption)); }); var each$30 = each$1; var ATTR$2 = '\0_ec_hist_store'; /** * @param {module:echarts/model/Global} ecModel * @param {Object} newSnapshot {dataZoomId, batch: [payloadInfo, ...]} */ function push(ecModel, newSnapshot) { var store = giveStore$1(ecModel); // If previous dataZoom can not be found, // complete an range with current range. each$30(newSnapshot, function (batchItem, dataZoomId) { var i = store.length - 1; for (; i >= 0; i--) { var snapshot = store[i]; if (snapshot[dataZoomId]) { break; } } if (i < 0) { // No origin range set, create one by current range. var dataZoomModel = ecModel.queryComponents( {mainType: 'dataZoom', subType: 'select', id: dataZoomId} )[0]; if (dataZoomModel) { var percentRange = dataZoomModel.getPercentRange(); store[0][dataZoomId] = { dataZoomId: dataZoomId, start: percentRange[0], end: percentRange[1] }; } } }); store.push(newSnapshot); } /** * @param {module:echarts/model/Global} ecModel * @return {Object} snapshot */ function pop(ecModel) { var store = giveStore$1(ecModel); var head = store[store.length - 1]; store.length > 1 && store.pop(); // Find top for all dataZoom. var snapshot = {}; each$30(head, function (batchItem, dataZoomId) { for (var i = store.length - 1; i >= 0; i--) { var batchItem = store[i][dataZoomId]; if (batchItem) { snapshot[dataZoomId] = batchItem; break; } } }); return snapshot; } /** * @param {module:echarts/model/Global} ecModel */ function clear$1(ecModel) { ecModel[ATTR$2] = null; } /** * @param {module:echarts/model/Global} ecModel * @return {number} records. always >= 1. */ function count(ecModel) { return giveStore$1(ecModel).length; } /** * [{key: dataZoomId, value: {dataZoomId, range}}, ...] * History length of each dataZoom may be different. * this._history[0] is used to store origin range. * @type {Array.<Object>} */ function giveStore$1(ecModel) { var store = ecModel[ATTR$2]; if (!store) { store = ecModel[ATTR$2] = [{}]; } return store; } DataZoomModel.extend({ type: 'dataZoom.select' }); DataZoomView.extend({ type: 'dataZoom.select' }); /** * DataZoom component entry */ // Use dataZoomSelect var dataZoomLang = lang.toolbox.dataZoom; var each$29 = each$1; // Spectial component id start with \0ec\0, see echarts/model/Global.js~hasInnerId var DATA_ZOOM_ID_BASE = '\0_ec_\0toolbox-dataZoom_'; function DataZoom(model, ecModel, api) { /** * @private * @type {module:echarts/component/helper/BrushController} */ (this._brushController = new BrushController(api.getZr())) .on('brush', bind(this._onBrush, this)) .mount(); /** * @private * @type {boolean} */ this._isZoomActive; } DataZoom.defaultOption = { show: true, // Icon group icon: { zoom: 'M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1', back: 'M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26' }, // `zoom`, `back` title: clone(dataZoomLang.title) }; var proto$6 = DataZoom.prototype; proto$6.render = function (featureModel, ecModel, api, payload) { this.model = featureModel; this.ecModel = ecModel; this.api = api; updateZoomBtnStatus(featureModel, ecModel, this, payload, api); updateBackBtnStatus(featureModel, ecModel); }; proto$6.onclick = function (ecModel, api, type) { handlers$1[type].call(this); }; proto$6.remove = function (ecModel, api) { this._brushController.unmount(); }; proto$6.dispose = function (ecModel, api) { this._brushController.dispose(); }; /** * @private */ var handlers$1 = { zoom: function () { var nextActive = !this._isZoomActive; this.api.dispatchAction({ type: 'takeGlobalCursor', key: 'dataZoomSelect', dataZoomSelectActive: nextActive }); }, back: function () { this._dispatchZoomAction(pop(this.ecModel)); } }; /** * @private */ proto$6._onBrush = function (areas, opt) { if (!opt.isEnd || !areas.length) { return; } var snapshot = {}; var ecModel = this.ecModel; this._brushController.updateCovers([]); // remove cover var brushTargetManager = new BrushTargetManager( retrieveAxisSetting(this.model.option), ecModel, {include: ['grid']} ); brushTargetManager.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys) { if (coordSys.type !== 'cartesian2d') { return; } var brushType = area.brushType; if (brushType === 'rect') { setBatch('x', coordSys, coordRange[0]); setBatch('y', coordSys, coordRange[1]); } else { setBatch(({lineX: 'x', lineY: 'y'})[brushType], coordSys, coordRange); } }); push(ecModel, snapshot); this._dispatchZoomAction(snapshot); function setBatch(dimName, coordSys, minMax) { var axis = coordSys.getAxis(dimName); var axisModel = axis.model; var dataZoomModel = findDataZoom(dimName, axisModel, ecModel); // Restrict range. var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan(); if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) { minMax = sliderMove( 0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan ); } dataZoomModel && (snapshot[dataZoomModel.id] = { dataZoomId: dataZoomModel.id, startValue: minMax[0], endValue: minMax[1] }); } function findDataZoom(dimName, axisModel, ecModel) { var found; ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, function (dzModel) { var has = dzModel.getAxisModel(dimName, axisModel.componentIndex); has && (found = dzModel); }); return found; } }; /** * @private */ proto$6._dispatchZoomAction = function (snapshot) { var batch = []; // Convert from hash map to array. each$29(snapshot, function (batchItem, dataZoomId) { batch.push(clone(batchItem)); }); batch.length && this.api.dispatchAction({ type: 'dataZoom', from: this.uid, batch: batch }); }; function retrieveAxisSetting(option) { var setting = {}; // Compatible with previous setting: null => all axis, false => no axis. each$1(['xAxisIndex', 'yAxisIndex'], function (name) { setting[name] = option[name]; setting[name] == null && (setting[name] = 'all'); (setting[name] === false || setting[name] === 'none') && (setting[name] = []); }); return setting; } function updateBackBtnStatus(featureModel, ecModel) { featureModel.setIconStatus( 'back', count(ecModel) > 1 ? 'emphasis' : 'normal' ); } function updateZoomBtnStatus(featureModel, ecModel, view, payload, api) { var zoomActive = view._isZoomActive; if (payload && payload.type === 'takeGlobalCursor') { zoomActive = payload.key === 'dataZoomSelect' ? payload.dataZoomSelectActive : false; } view._isZoomActive = zoomActive; featureModel.setIconStatus('zoom', zoomActive ? 'emphasis' : 'normal'); var brushTargetManager = new BrushTargetManager( retrieveAxisSetting(featureModel.option), ecModel, {include: ['grid']} ); view._brushController .setPanels(brushTargetManager.makePanelOpts(api, function (targetInfo) { return (targetInfo.xAxisDeclared && !targetInfo.yAxisDeclared) ? 'lineX' : (!targetInfo.xAxisDeclared && targetInfo.yAxisDeclared) ? 'lineY' : 'rect'; })) .enableBrush( zoomActive ? { brushType: 'auto', brushStyle: { // FIXME user customized? lineWidth: 0, fill: 'rgba(0,0,0,0.2)' } } : false ); } register$1('dataZoom', DataZoom); // Create special dataZoom option for select registerPreprocessor(function (option) { if (!option) { return; } var dataZoomOpts = option.dataZoom || (option.dataZoom = []); if (!isArray(dataZoomOpts)) { option.dataZoom = dataZoomOpts = [dataZoomOpts]; } var toolboxOpt = option.toolbox; if (toolboxOpt) { // Assume there is only one toolbox if (isArray(toolboxOpt)) { toolboxOpt = toolboxOpt[0]; } if (toolboxOpt && toolboxOpt.feature) { var dataZoomOpt = toolboxOpt.feature.dataZoom; addForAxis('xAxis', dataZoomOpt); addForAxis('yAxis', dataZoomOpt); } } function addForAxis(axisName, dataZoomOpt) { if (!dataZoomOpt) { return; } // Try not to modify model, because it is not merged yet. var axisIndicesName = axisName + 'Index'; var givenAxisIndices = dataZoomOpt[axisIndicesName]; if (givenAxisIndices != null && givenAxisIndices != 'all' && !isArray(givenAxisIndices) ) { givenAxisIndices = (givenAxisIndices === false || givenAxisIndices === 'none') ? [] : [givenAxisIndices]; } forEachComponent(axisName, function (axisOpt, axisIndex) { if (givenAxisIndices != null && givenAxisIndices != 'all' && indexOf(givenAxisIndices, axisIndex) === -1 ) { return; } var newOpt = { type: 'select', $fromToolbox: true, // Id for merge mapping. id: DATA_ZOOM_ID_BASE + axisName + axisIndex }; // FIXME // Only support one axis now. newOpt[axisIndicesName] = axisIndex; dataZoomOpts.push(newOpt); }); } function forEachComponent(mainType, cb) { var opts = option[mainType]; if (!isArray(opts)) { opts = opts ? [opts] : []; } each$29(opts, cb); } }); var restoreLang = lang.toolbox.restore; function Restore(model) { this.model = model; } Restore.defaultOption = { show: true, icon: 'M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5', title: restoreLang.title }; var proto$7 = Restore.prototype; proto$7.onclick = function (ecModel, api, type) { clear$1(ecModel); api.dispatchAction({ type: 'restore', from: this.uid }); }; register$1('restore', Restore); registerAction( {type: 'restore', event: 'restore', update: 'prepareAndUpdate'}, function (payload, ecModel) { ecModel.resetOption('recreate'); } ); var urn = 'urn:schemas-microsoft-com:vml'; var win = typeof window === 'undefined' ? null : window; var vmlInited = false; var doc = win && win.document; function createNode(tagName) { return doCreateNode(tagName); } // Avoid assign to an exported variable, for transforming to cjs. var doCreateNode; if (doc && !env$1.canvasSupported) { try { !doc.namespaces.zrvml && doc.namespaces.add('zrvml', urn); doCreateNode = function (tagName) { return doc.createElement('<zrvml:' + tagName + ' class="zrvml">'); }; } catch (e) { doCreateNode = function (tagName) { return doc.createElement('<' + tagName + ' xmlns="' + urn + '" class="zrvml">'); }; } } // From raphael function initVML() { if (vmlInited || !doc) { return; } vmlInited = true; var styleSheets = doc.styleSheets; if (styleSheets.length < 31) { doc.createStyleSheet().addRule('.zrvml', 'behavior:url(#default#VML)'); } else { // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx styleSheets[0].addRule('.zrvml', 'behavior:url(#default#VML)'); } } // http://www.w3.org/TR/NOTE-VML // TODO Use proxy like svg instead of overwrite brush methods var CMD$3 = PathProxy.CMD; var round$3 = Math.round; var sqrt = Math.sqrt; var abs$1 = Math.abs; var cos = Math.cos; var sin = Math.sin; var mathMax$8 = Math.max; if (!env$1.canvasSupported) { var comma = ','; var imageTransformPrefix = 'progid:DXImageTransform.Microsoft'; var Z = 21600; var Z2 = Z / 2; var ZLEVEL_BASE = 100000; var Z_BASE$1 = 1000; var initRootElStyle = function (el) { el.style.cssText = 'position:absolute;left:0;top:0;width:1px;height:1px;'; el.coordsize = Z + ',' + Z; el.coordorigin = '0,0'; }; var encodeHtmlAttribute = function (s) { return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;'); }; var rgb2Str = function (r, g, b) { return 'rgb(' + [r, g, b].join(',') + ')'; }; var append = function (parent, child) { if (child && parent && child.parentNode !== parent) { parent.appendChild(child); } }; var remove = function (parent, child) { if (child && parent && child.parentNode === parent) { parent.removeChild(child); } }; var getZIndex = function (zlevel, z, z2) { // z 的取值范围为 [0, 1000] return (parseFloat(zlevel) || 0) * ZLEVEL_BASE + (parseFloat(z) || 0) * Z_BASE$1 + z2; }; var parsePercent$3 = function (value, maxValue) { if (typeof value === 'string') { if (value.lastIndexOf('%') >= 0) { return parseFloat(value) / 100 * maxValue; } return parseFloat(value); } return value; }; /*************************************************** * PATH **************************************************/ var setColorAndOpacity = function (el, color, opacity) { var colorArr = parse(color); opacity = +opacity; if (isNaN(opacity)) { opacity = 1; } if (colorArr) { el.color = rgb2Str(colorArr[0], colorArr[1], colorArr[2]); el.opacity = opacity * colorArr[3]; } }; var getColorAndAlpha = function (color) { var colorArr = parse(color); return [ rgb2Str(colorArr[0], colorArr[1], colorArr[2]), colorArr[3] ]; }; var updateFillNode = function (el, style, zrEl) { // TODO pattern var fill = style.fill; if (fill != null) { // Modified from excanvas if (fill instanceof Gradient) { var gradientType; var angle = 0; var focus = [0, 0]; // additional offset var shift = 0; // scale factor for offset var expansion = 1; var rect = zrEl.getBoundingRect(); var rectWidth = rect.width; var rectHeight = rect.height; if (fill.type === 'linear') { gradientType = 'gradient'; var transform = zrEl.transform; var p0 = [fill.x * rectWidth, fill.y * rectHeight]; var p1 = [fill.x2 * rectWidth, fill.y2 * rectHeight]; if (transform) { applyTransform(p0, p0, transform); applyTransform(p1, p1, transform); } var dx = p1[0] - p0[0]; var dy = p1[1] - p0[1]; angle = Math.atan2(dx, dy) * 180 / Math.PI; // The angle should be a non-negative number. if (angle < 0) { angle += 360; } // Very small angles produce an unexpected result because they are // converted to a scientific notation string. if (angle < 1e-6) { angle = 0; } } else { gradientType = 'gradientradial'; var p0 = [fill.x * rectWidth, fill.y * rectHeight]; var transform = zrEl.transform; var scale$$1 = zrEl.scale; var width = rectWidth; var height = rectHeight; focus = [ // Percent in bounding rect (p0[0] - rect.x) / width, (p0[1] - rect.y) / height ]; if (transform) { applyTransform(p0, p0, transform); } width /= scale$$1[0] * Z; height /= scale$$1[1] * Z; var dimension = mathMax$8(width, height); shift = 2 * 0 / dimension; expansion = 2 * fill.r / dimension - shift; } // We need to sort the color stops in ascending order by offset, // otherwise IE won't interpret it correctly. var stops = fill.colorStops.slice(); stops.sort(function(cs1, cs2) { return cs1.offset - cs2.offset; }); var length$$1 = stops.length; // Color and alpha list of first and last stop var colorAndAlphaList = []; var colors = []; for (var i = 0; i < length$$1; i++) { var stop = stops[i]; var colorAndAlpha = getColorAndAlpha(stop.color); colors.push(stop.offset * expansion + shift + ' ' + colorAndAlpha[0]); if (i === 0 || i === length$$1 - 1) { colorAndAlphaList.push(colorAndAlpha); } } if (length$$1 >= 2) { var color1 = colorAndAlphaList[0][0]; var color2 = colorAndAlphaList[1][0]; var opacity1 = colorAndAlphaList[0][1] * style.opacity; var opacity2 = colorAndAlphaList[1][1] * style.opacity; el.type = gradientType; el.method = 'none'; el.focus = '100%'; el.angle = angle; el.color = color1; el.color2 = color2; el.colors = colors.join(','); // When colors attribute is used, the meanings of opacity and o:opacity2 // are reversed. el.opacity = opacity2; // FIXME g_o_:opacity ? el.opacity2 = opacity1; } if (gradientType === 'radial') { el.focusposition = focus.join(','); } } else { // FIXME Change from Gradient fill to color fill setColorAndOpacity(el, fill, style.opacity); } } }; var updateStrokeNode = function (el, style) { // if (style.lineJoin != null) { // el.joinstyle = style.lineJoin; // } // if (style.miterLimit != null) { // el.miterlimit = style.miterLimit * Z; // } // if (style.lineCap != null) { // el.endcap = style.lineCap; // } if (style.lineDash != null) { el.dashstyle = style.lineDash.join(' '); } if (style.stroke != null && !(style.stroke instanceof Gradient)) { setColorAndOpacity(el, style.stroke, style.opacity); } }; var updateFillAndStroke = function (vmlEl, type, style, zrEl) { var isFill = type == 'fill'; var el = vmlEl.getElementsByTagName(type)[0]; // Stroke must have lineWidth if (style[type] != null && style[type] !== 'none' && (isFill || (!isFill && style.lineWidth))) { vmlEl[isFill ? 'filled' : 'stroked'] = 'true'; // FIXME Remove before updating, or set `colors` will throw error if (style[type] instanceof Gradient) { remove(vmlEl, el); } if (!el) { el = createNode(type); } isFill ? updateFillNode(el, style, zrEl) : updateStrokeNode(el, style); append(vmlEl, el); } else { vmlEl[isFill ? 'filled' : 'stroked'] = 'false'; remove(vmlEl, el); } }; var points$3 = [[], [], []]; var pathDataToString = function (path, m) { var M = CMD$3.M; var C = CMD$3.C; var L = CMD$3.L; var A = CMD$3.A; var Q = CMD$3.Q; var str = []; var nPoint; var cmdStr; var cmd; var i; var xi; var yi; var data = path.data; var dataLength = path.len(); for (i = 0; i < dataLength;) { cmd = data[i++]; cmdStr = ''; nPoint = 0; switch (cmd) { case M: cmdStr = ' m '; nPoint = 1; xi = data[i++]; yi = data[i++]; points$3[0][0] = xi; points$3[0][1] = yi; break; case L: cmdStr = ' l '; nPoint = 1; xi = data[i++]; yi = data[i++]; points$3[0][0] = xi; points$3[0][1] = yi; break; case Q: case C: cmdStr = ' c '; nPoint = 3; var x1 = data[i++]; var y1 = data[i++]; var x2 = data[i++]; var y2 = data[i++]; var x3; var y3; if (cmd === Q) { // Convert quadratic to cubic using degree elevation x3 = x2; y3 = y2; x2 = (x2 + 2 * x1) / 3; y2 = (y2 + 2 * y1) / 3; x1 = (xi + 2 * x1) / 3; y1 = (yi + 2 * y1) / 3; } else { x3 = data[i++]; y3 = data[i++]; } points$3[0][0] = x1; points$3[0][1] = y1; points$3[1][0] = x2; points$3[1][1] = y2; points$3[2][0] = x3; points$3[2][1] = y3; xi = x3; yi = y3; break; case A: var x = 0; var y = 0; var sx = 1; var sy = 1; var angle = 0; if (m) { // Extract SRT from matrix x = m[4]; y = m[5]; sx = sqrt(m[0] * m[0] + m[1] * m[1]); sy = sqrt(m[2] * m[2] + m[3] * m[3]); angle = Math.atan2(-m[1] / sy, m[0] / sx); } var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var startAngle = data[i++] + angle; var endAngle = data[i++] + startAngle + angle; // FIXME // var psi = data[i++]; i++; var clockwise = data[i++]; var x0 = cx + cos(startAngle) * rx; var y0 = cy + sin(startAngle) * ry; var x1 = cx + cos(endAngle) * rx; var y1 = cy + sin(endAngle) * ry; var type = clockwise ? ' wa ' : ' at '; if (Math.abs(x0 - x1) < 1e-4) { // IE won't render arches drawn counter clockwise if x0 == x1. if (Math.abs(endAngle - startAngle) > 1e-2) { // Offset x0 by 1/80 of a pixel. Use something // that can be represented in binary if (clockwise) { x0 += 270 / Z; } } else { // Avoid case draw full circle if (Math.abs(y0 - cy) < 1e-4) { if ((clockwise && x0 < cx) || (!clockwise && x0 > cx)) { y1 -= 270 / Z; } else { y1 += 270 / Z; } } else if ((clockwise && y0 < cy) || (!clockwise && y0 > cy)) { x1 += 270 / Z; } else { x1 -= 270 / Z; } } } str.push( type, round$3(((cx - rx) * sx + x) * Z - Z2), comma, round$3(((cy - ry) * sy + y) * Z - Z2), comma, round$3(((cx + rx) * sx + x) * Z - Z2), comma, round$3(((cy + ry) * sy + y) * Z - Z2), comma, round$3((x0 * sx + x) * Z - Z2), comma, round$3((y0 * sy + y) * Z - Z2), comma, round$3((x1 * sx + x) * Z - Z2), comma, round$3((y1 * sy + y) * Z - Z2) ); xi = x1; yi = y1; break; case CMD$3.R: var p0 = points$3[0]; var p1 = points$3[1]; // x0, y0 p0[0] = data[i++]; p0[1] = data[i++]; // x1, y1 p1[0] = p0[0] + data[i++]; p1[1] = p0[1] + data[i++]; if (m) { applyTransform(p0, p0, m); applyTransform(p1, p1, m); } p0[0] = round$3(p0[0] * Z - Z2); p1[0] = round$3(p1[0] * Z - Z2); p0[1] = round$3(p0[1] * Z - Z2); p1[1] = round$3(p1[1] * Z - Z2); str.push( // x0, y0 ' m ', p0[0], comma, p0[1], // x1, y0 ' l ', p1[0], comma, p0[1], // x1, y1 ' l ', p1[0], comma, p1[1], // x0, y1 ' l ', p0[0], comma, p1[1] ); break; case CMD$3.Z: // FIXME Update xi, yi str.push(' x '); } if (nPoint > 0) { str.push(cmdStr); for (var k = 0; k < nPoint; k++) { var p = points$3[k]; m && applyTransform(p, p, m); // 不 round 会非常慢 str.push( round$3(p[0] * Z - Z2), comma, round$3(p[1] * Z - Z2), k < nPoint - 1 ? comma : '' ); } } } return str.join(''); }; // Rewrite the original path method Path.prototype.brushVML = function (vmlRoot) { var style = this.style; var vmlEl = this._vmlEl; if (!vmlEl) { vmlEl = createNode('shape'); initRootElStyle(vmlEl); this._vmlEl = vmlEl; } updateFillAndStroke(vmlEl, 'fill', style, this); updateFillAndStroke(vmlEl, 'stroke', style, this); var m = this.transform; var needTransform = m != null; var strokeEl = vmlEl.getElementsByTagName('stroke')[0]; if (strokeEl) { var lineWidth = style.lineWidth; // Get the line scale. // Determinant of this.m_ means how much the area is enlarged by the // transformation. So its square root can be used as a scale factor // for width. if (needTransform && !style.strokeNoScale) { var det = m[0] * m[3] - m[1] * m[2]; lineWidth *= sqrt(abs$1(det)); } strokeEl.weight = lineWidth + 'px'; } var path = this.path || (this.path = new PathProxy()); if (this.__dirtyPath) { path.beginPath(); this.buildPath(path, this.shape); path.toStatic(); this.__dirtyPath = false; } vmlEl.path = pathDataToString(path, this.transform); vmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Append to root append(vmlRoot, vmlEl); // Text if (style.text != null) { this.drawRectText(vmlRoot, this.getBoundingRect()); } else { this.removeRectText(vmlRoot); } }; Path.prototype.onRemove = function (vmlRoot) { remove(vmlRoot, this._vmlEl); this.removeRectText(vmlRoot); }; Path.prototype.onAdd = function (vmlRoot) { append(vmlRoot, this._vmlEl); this.appendRectText(vmlRoot); }; /*************************************************** * IMAGE **************************************************/ var isImage = function (img) { // FIXME img instanceof Image 如果 img 是一个字符串的时候,IE8 下会报错 return (typeof img === 'object') && img.tagName && img.tagName.toUpperCase() === 'IMG'; // return img instanceof Image; }; // Rewrite the original path method ZImage.prototype.brushVML = function (vmlRoot) { var style = this.style; var image = style.image; // Image original width, height var ow; var oh; if (isImage(image)) { var src = image.src; if (src === this._imageSrc) { ow = this._imageWidth; oh = this._imageHeight; } else { var imageRuntimeStyle = image.runtimeStyle; var oldRuntimeWidth = imageRuntimeStyle.width; var oldRuntimeHeight = imageRuntimeStyle.height; imageRuntimeStyle.width = 'auto'; imageRuntimeStyle.height = 'auto'; // get the original size ow = image.width; oh = image.height; // and remove overides imageRuntimeStyle.width = oldRuntimeWidth; imageRuntimeStyle.height = oldRuntimeHeight; // Caching image original width, height and src this._imageSrc = src; this._imageWidth = ow; this._imageHeight = oh; } image = src; } else { if (image === this._imageSrc) { ow = this._imageWidth; oh = this._imageHeight; } } if (!image) { return; } var x = style.x || 0; var y = style.y || 0; var dw = style.width; var dh = style.height; var sw = style.sWidth; var sh = style.sHeight; var sx = style.sx || 0; var sy = style.sy || 0; var hasCrop = sw && sh; var vmlEl = this._vmlEl; if (!vmlEl) { // FIXME 使用 group 在 left, top 都不是 0 的时候就无法显示了。 // vmlEl = vmlCore.createNode('group'); vmlEl = doc.createElement('div'); initRootElStyle(vmlEl); this._vmlEl = vmlEl; } var vmlElStyle = vmlEl.style; var hasRotation = false; var m; var scaleX = 1; var scaleY = 1; if (this.transform) { m = this.transform; scaleX = sqrt(m[0] * m[0] + m[1] * m[1]); scaleY = sqrt(m[2] * m[2] + m[3] * m[3]); hasRotation = m[1] || m[2]; } if (hasRotation) { // If filters are necessary (rotation exists), create them // filters are bog-slow, so only create them if abbsolutely necessary // The following check doesn't account for skews (which don't exist // in the canvas spec (yet) anyway. // From excanvas var p0 = [x, y]; var p1 = [x + dw, y]; var p2 = [x, y + dh]; var p3 = [x + dw, y + dh]; applyTransform(p0, p0, m); applyTransform(p1, p1, m); applyTransform(p2, p2, m); applyTransform(p3, p3, m); var maxX = mathMax$8(p0[0], p1[0], p2[0], p3[0]); var maxY = mathMax$8(p0[1], p1[1], p2[1], p3[1]); var transformFilter = []; transformFilter.push('M11=', m[0] / scaleX, comma, 'M12=', m[2] / scaleY, comma, 'M21=', m[1] / scaleX, comma, 'M22=', m[3] / scaleY, comma, 'Dx=', round$3(x * scaleX + m[4]), comma, 'Dy=', round$3(y * scaleY + m[5])); vmlElStyle.padding = '0 ' + round$3(maxX) + 'px ' + round$3(maxY) + 'px 0'; // FIXME DXImageTransform 在 IE11 的兼容模式下不起作用 vmlElStyle.filter = imageTransformPrefix + '.Matrix(' + transformFilter.join('') + ', SizingMethod=clip)'; } else { if (m) { x = x * scaleX + m[4]; y = y * scaleY + m[5]; } vmlElStyle.filter = ''; vmlElStyle.left = round$3(x) + 'px'; vmlElStyle.top = round$3(y) + 'px'; } var imageEl = this._imageEl; var cropEl = this._cropEl; if (!imageEl) { imageEl = doc.createElement('div'); this._imageEl = imageEl; } var imageELStyle = imageEl.style; if (hasCrop) { // Needs know image original width and height if (! (ow && oh)) { var tmpImage = new Image(); var self = this; tmpImage.onload = function () { tmpImage.onload = null; ow = tmpImage.width; oh = tmpImage.height; // Adjust image width and height to fit the ratio destinationSize / sourceSize imageELStyle.width = round$3(scaleX * ow * dw / sw) + 'px'; imageELStyle.height = round$3(scaleY * oh * dh / sh) + 'px'; // Caching image original width, height and src self._imageWidth = ow; self._imageHeight = oh; self._imageSrc = image; }; tmpImage.src = image; } else { imageELStyle.width = round$3(scaleX * ow * dw / sw) + 'px'; imageELStyle.height = round$3(scaleY * oh * dh / sh) + 'px'; } if (! cropEl) { cropEl = doc.createElement('div'); cropEl.style.overflow = 'hidden'; this._cropEl = cropEl; } var cropElStyle = cropEl.style; cropElStyle.width = round$3((dw + sx * dw / sw) * scaleX); cropElStyle.height = round$3((dh + sy * dh / sh) * scaleY); cropElStyle.filter = imageTransformPrefix + '.Matrix(Dx=' + (-sx * dw / sw * scaleX) + ',Dy=' + (-sy * dh / sh * scaleY) + ')'; if (! cropEl.parentNode) { vmlEl.appendChild(cropEl); } if (imageEl.parentNode != cropEl) { cropEl.appendChild(imageEl); } } else { imageELStyle.width = round$3(scaleX * dw) + 'px'; imageELStyle.height = round$3(scaleY * dh) + 'px'; vmlEl.appendChild(imageEl); if (cropEl && cropEl.parentNode) { vmlEl.removeChild(cropEl); this._cropEl = null; } } var filterStr = ''; var alpha = style.opacity; if (alpha < 1) { filterStr += '.Alpha(opacity=' + round$3(alpha * 100) + ') '; } filterStr += imageTransformPrefix + '.AlphaImageLoader(src=' + image + ', SizingMethod=scale)'; imageELStyle.filter = filterStr; vmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Append to root append(vmlRoot, vmlEl); // Text if (style.text != null) { this.drawRectText(vmlRoot, this.getBoundingRect()); } }; ZImage.prototype.onRemove = function (vmlRoot) { remove(vmlRoot, this._vmlEl); this._vmlEl = null; this._cropEl = null; this._imageEl = null; this.removeRectText(vmlRoot); }; ZImage.prototype.onAdd = function (vmlRoot) { append(vmlRoot, this._vmlEl); this.appendRectText(vmlRoot); }; /*************************************************** * TEXT **************************************************/ var DEFAULT_STYLE_NORMAL = 'normal'; var fontStyleCache = {}; var fontStyleCacheCount = 0; var MAX_FONT_CACHE_SIZE = 100; var fontEl = document.createElement('div'); var getFontStyle = function (fontString) { var fontStyle = fontStyleCache[fontString]; if (!fontStyle) { // Clear cache if (fontStyleCacheCount > MAX_FONT_CACHE_SIZE) { fontStyleCacheCount = 0; fontStyleCache = {}; } var style = fontEl.style; var fontFamily; try { style.font = fontString; fontFamily = style.fontFamily.split(',')[0]; } catch (e) { } fontStyle = { style: style.fontStyle || DEFAULT_STYLE_NORMAL, variant: style.fontVariant || DEFAULT_STYLE_NORMAL, weight: style.fontWeight || DEFAULT_STYLE_NORMAL, size: parseFloat(style.fontSize || 12) | 0, family: fontFamily || 'Microsoft YaHei' }; fontStyleCache[fontString] = fontStyle; fontStyleCacheCount++; } return fontStyle; }; var textMeasureEl; // Overwrite measure text method $override$1('measureText', function (text, textFont) { var doc$$1 = doc; if (!textMeasureEl) { textMeasureEl = doc$$1.createElement('div'); textMeasureEl.style.cssText = 'position:absolute;top:-20000px;left:0;' + 'padding:0;margin:0;border:none;white-space:pre;'; doc.body.appendChild(textMeasureEl); } try { textMeasureEl.style.font = textFont; } catch (ex) { // Ignore failures to set to invalid font. } textMeasureEl.innerHTML = ''; // Don't use innerHTML or innerText because they allow markup/whitespace. textMeasureEl.appendChild(doc$$1.createTextNode(text)); return { width: textMeasureEl.offsetWidth }; }); var tmpRect$2 = new BoundingRect(); var drawRectText = function (vmlRoot, rect, textRect, fromTextEl) { var style = this.style; // Optimize, avoid normalize every time. this.__dirty && normalizeTextStyle(style, true); var text = style.text; // Convert to string text != null && (text += ''); if (!text) { return; } // Convert rich text to plain text. Rich text is not supported in // IE8-, but tags in rich text template will be removed. if (style.rich) { var contentBlock = parseRichText(text, style); text = []; for (var i = 0; i < contentBlock.lines.length; i++) { var tokens = contentBlock.lines[i].tokens; var textLine = []; for (var j = 0; j < tokens.length; j++) { textLine.push(tokens[j].text); } text.push(textLine.join('')); } text = text.join('\n'); } var x; var y; var align = style.textAlign; var verticalAlign = style.textVerticalAlign; var fontStyle = getFontStyle(style.font); // FIXME encodeHtmlAttribute ? var font = fontStyle.style + ' ' + fontStyle.variant + ' ' + fontStyle.weight + ' ' + fontStyle.size + 'px "' + fontStyle.family + '"'; textRect = textRect || getBoundingRect(text, font, align, verticalAlign); // Transform rect to view space var m = this.transform; // Ignore transform for text in other element if (m && !fromTextEl) { tmpRect$2.copy(rect); tmpRect$2.applyTransform(m); rect = tmpRect$2; } if (!fromTextEl) { var textPosition = style.textPosition; var distance$$1 = style.textDistance; // Text position represented by coord if (textPosition instanceof Array) { x = rect.x + parsePercent$3(textPosition[0], rect.width); y = rect.y + parsePercent$3(textPosition[1], rect.height); align = align || 'left'; } else { var res = adjustTextPositionOnRect( textPosition, rect, distance$$1 ); x = res.x; y = res.y; // Default align and baseline when has textPosition align = align || res.textAlign; verticalAlign = verticalAlign || res.textVerticalAlign; } } else { x = rect.x; y = rect.y; } x = adjustTextX(x, textRect.width, align); y = adjustTextY(y, textRect.height, verticalAlign); // Force baseline 'middle' y += textRect.height / 2; // var fontSize = fontStyle.size; // 1.75 is an arbitrary number, as there is no info about the text baseline // switch (baseline) { // case 'hanging': // case 'top': // y += fontSize / 1.75; // break; // case 'middle': // break; // default: // // case null: // // case 'alphabetic': // // case 'ideographic': // // case 'bottom': // y -= fontSize / 2.25; // break; // } // switch (align) { // case 'left': // break; // case 'center': // x -= textRect.width / 2; // break; // case 'right': // x -= textRect.width; // break; // case 'end': // align = elementStyle.direction == 'ltr' ? 'right' : 'left'; // break; // case 'start': // align = elementStyle.direction == 'rtl' ? 'right' : 'left'; // break; // default: // align = 'left'; // } var createNode$$1 = createNode; var textVmlEl = this._textVmlEl; var pathEl; var textPathEl; var skewEl; if (!textVmlEl) { textVmlEl = createNode$$1('line'); pathEl = createNode$$1('path'); textPathEl = createNode$$1('textpath'); skewEl = createNode$$1('skew'); // FIXME Why here is not cammel case // Align 'center' seems wrong textPathEl.style['v-text-align'] = 'left'; initRootElStyle(textVmlEl); pathEl.textpathok = true; textPathEl.on = true; textVmlEl.from = '0 0'; textVmlEl.to = '1000 0.05'; append(textVmlEl, skewEl); append(textVmlEl, pathEl); append(textVmlEl, textPathEl); this._textVmlEl = textVmlEl; } else { // 这里是在前面 appendChild 保证顺序的前提下 skewEl = textVmlEl.firstChild; pathEl = skewEl.nextSibling; textPathEl = pathEl.nextSibling; } var coords = [x, y]; var textVmlElStyle = textVmlEl.style; // Ignore transform for text in other element if (m && fromTextEl) { applyTransform(coords, coords, m); skewEl.on = true; skewEl.matrix = m[0].toFixed(3) + comma + m[2].toFixed(3) + comma + m[1].toFixed(3) + comma + m[3].toFixed(3) + ',0,0'; // Text position skewEl.offset = (round$3(coords[0]) || 0) + ',' + (round$3(coords[1]) || 0); // Left top point as origin skewEl.origin = '0 0'; textVmlElStyle.left = '0px'; textVmlElStyle.top = '0px'; } else { skewEl.on = false; textVmlElStyle.left = round$3(x) + 'px'; textVmlElStyle.top = round$3(y) + 'px'; } textPathEl.string = encodeHtmlAttribute(text); // TODO try { textPathEl.style.font = font; } // Error font format catch (e) {} updateFillAndStroke(textVmlEl, 'fill', { fill: style.textFill, opacity: style.opacity }, this); updateFillAndStroke(textVmlEl, 'stroke', { stroke: style.textStroke, opacity: style.opacity, lineDash: style.lineDash }, this); textVmlEl.style.zIndex = getZIndex(this.zlevel, this.z, this.z2); // Attached to root append(vmlRoot, textVmlEl); }; var removeRectText = function (vmlRoot) { remove(vmlRoot, this._textVmlEl); this._textVmlEl = null; }; var appendRectText = function (vmlRoot) { append(vmlRoot, this._textVmlEl); }; var list = [RectText, Displayable, ZImage, Path, Text]; // In case Displayable has been mixed in RectText for (var i$3 = 0; i$3 < list.length; i$3++) { var proto$8 = list[i$3].prototype; proto$8.drawRectText = drawRectText; proto$8.removeRectText = removeRectText; proto$8.appendRectText = appendRectText; } Text.prototype.brushVML = function (vmlRoot) { var style = this.style; if (style.text != null) { this.drawRectText(vmlRoot, { x: style.x || 0, y: style.y || 0, width: 0, height: 0 }, this.getBoundingRect(), true); } else { this.removeRectText(vmlRoot); } }; Text.prototype.onRemove = function (vmlRoot) { this.removeRectText(vmlRoot); }; Text.prototype.onAdd = function (vmlRoot) { this.appendRectText(vmlRoot); }; } /** * VML Painter. * * @module zrender/vml/Painter */ function parseInt10$1(val) { return parseInt(val, 10); } /** * @alias module:zrender/vml/Painter */ function VMLPainter(root, storage) { initVML(); this.root = root; this.storage = storage; var vmlViewport = document.createElement('div'); var vmlRoot = document.createElement('div'); vmlViewport.style.cssText = 'display:inline-block;overflow:hidden;position:relative;width:300px;height:150px;'; vmlRoot.style.cssText = 'position:absolute;left:0;top:0;'; root.appendChild(vmlViewport); this._vmlRoot = vmlRoot; this._vmlViewport = vmlViewport; this.resize(); // Modify storage var oldDelFromStorage = storage.delFromStorage; var oldAddToStorage = storage.addToStorage; storage.delFromStorage = function (el) { oldDelFromStorage.call(storage, el); if (el) { el.onRemove && el.onRemove(vmlRoot); } }; storage.addToStorage = function (el) { // Displayable already has a vml node el.onAdd && el.onAdd(vmlRoot); oldAddToStorage.call(storage, el); }; this._firstPaint = true; } VMLPainter.prototype = { constructor: VMLPainter, getType: function () { return 'vml'; }, /** * @return {HTMLDivElement} */ getViewportRoot: function () { return this._vmlViewport; }, getViewportRootOffset: function () { var viewportRoot = this.getViewportRoot(); if (viewportRoot) { return { offsetLeft: viewportRoot.offsetLeft || 0, offsetTop: viewportRoot.offsetTop || 0 }; } }, /** * 刷新 */ refresh: function () { var list = this.storage.getDisplayList(true, true); this._paintList(list); }, _paintList: function (list) { var vmlRoot = this._vmlRoot; for (var i = 0; i < list.length; i++) { var el = list[i]; if (el.invisible || el.ignore) { if (!el.__alreadyNotVisible) { el.onRemove(vmlRoot); } // Set as already invisible el.__alreadyNotVisible = true; } else { if (el.__alreadyNotVisible) { el.onAdd(vmlRoot); } el.__alreadyNotVisible = false; if (el.__dirty) { el.beforeBrush && el.beforeBrush(); (el.brushVML || el.brush).call(el, vmlRoot); el.afterBrush && el.afterBrush(); } } el.__dirty = false; } if (this._firstPaint) { // Detached from document at first time // to avoid page refreshing too many times // FIXME 如果每次都先 removeChild 可能会导致一些填充和描边的效果改变 this._vmlViewport.appendChild(vmlRoot); this._firstPaint = false; } }, resize: function (width, height) { var width = width == null ? this._getWidth() : width; var height = height == null ? this._getHeight() : height; if (this._width != width || this._height != height) { this._width = width; this._height = height; var vmlViewportStyle = this._vmlViewport.style; vmlViewportStyle.width = width + 'px'; vmlViewportStyle.height = height + 'px'; } }, dispose: function () { this.root.innerHTML = ''; this._vmlRoot = this._vmlViewport = this.storage = null; }, getWidth: function () { return this._width; }, getHeight: function () { return this._height; }, clear: function () { if (this._vmlViewport) { this.root.removeChild(this._vmlViewport); } }, _getWidth: function () { var root = this.root; var stl = root.currentStyle; return ((root.clientWidth || parseInt10$1(stl.width)) - parseInt10$1(stl.paddingLeft) - parseInt10$1(stl.paddingRight)) | 0; }, _getHeight: function () { var root = this.root; var stl = root.currentStyle; return ((root.clientHeight || parseInt10$1(stl.height)) - parseInt10$1(stl.paddingTop) - parseInt10$1(stl.paddingBottom)) | 0; } }; // Not supported methods function createMethodNotSupport(method) { return function () { zrLog('In IE8.0 VML mode painter not support method "' + method + '"'); }; } // Unsupported methods each$1([ 'getLayer', 'insertLayer', 'eachLayer', 'eachBuiltinLayer', 'eachOtherLayer', 'getLayers', 'modLayer', 'delLayer', 'clearLayer', 'toDataURL', 'pathToImage' ], function (name) { VMLPainter.prototype[name] = createMethodNotSupport(name); }); registerPainter('vml', VMLPainter); var svgURI = 'http://www.w3.org/2000/svg'; function createElement(name) { return document.createElementNS(svgURI, name); } // TODO // 1. shadow // 2. Image: sx, sy, sw, sh var CMD$4 = PathProxy.CMD; var arrayJoin = Array.prototype.join; var NONE = 'none'; var mathRound = Math.round; var mathSin$3 = Math.sin; var mathCos$3 = Math.cos; var PI$5 = Math.PI; var PI2$7 = Math.PI * 2; var degree = 180 / PI$5; var EPSILON$4 = 1e-4; function round4(val) { return mathRound(val * 1e4) / 1e4; } function isAroundZero$1(val) { return val < EPSILON$4 && val > -EPSILON$4; } function pathHasFill(style, isText) { var fill = isText ? style.textFill : style.fill; return fill != null && fill !== NONE; } function pathHasStroke(style, isText) { var stroke = isText ? style.textStroke : style.stroke; return stroke != null && stroke !== NONE; } function setTransform(svgEl, m) { if (m) { attr(svgEl, 'transform', 'matrix(' + arrayJoin.call(m, ',') + ')'); } } function attr(el, key, val) { if (!val || val.type !== 'linear' && val.type !== 'radial') { // Don't set attribute for gradient, since it need new dom nodes el.setAttribute(key, val); } } function attrXLink(el, key, val) { el.setAttributeNS('http://www.w3.org/1999/xlink', key, val); } function bindStyle(svgEl, style, isText) { if (pathHasFill(style, isText)) { var fill = isText ? style.textFill : style.fill; fill = fill === 'transparent' ? NONE : fill; /** * FIXME: * This is a temporary fix for Chrome's clipping bug * that happens when a clip-path is referring another one. * This fix should be used before Chrome's bug is fixed. * For an element that has clip-path, and fill is none, * set it to be "rgba(0, 0, 0, 0.002)" will hide the element. * Otherwise, it will show black fill color. * 0.002 is used because this won't work for alpha values smaller * than 0.002. * * See * https://bugs.chromium.org/p/chromium/issues/detail?id=659790 * for more information. */ if (svgEl.getAttribute('clip-path') !== 'none' && fill === NONE) { fill = 'rgba(0, 0, 0, 0.002)'; } attr(svgEl, 'fill', fill); attr(svgEl, 'fill-opacity', style.opacity); } else { attr(svgEl, 'fill', NONE); } if (pathHasStroke(style, isText)) { var stroke = isText ? style.textStroke : style.stroke; stroke = stroke === 'transparent' ? NONE : stroke; attr(svgEl, 'stroke', stroke); var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth; var strokeScale = !isText && style.strokeNoScale ? style.host.getLineScale() : 1; attr(svgEl, 'stroke-width', strokeWidth / strokeScale); // stroke then fill for text; fill then stroke for others attr(svgEl, 'paint-order', isText ? 'stroke' : 'fill'); attr(svgEl, 'stroke-opacity', style.opacity); var lineDash = style.lineDash; if (lineDash) { attr(svgEl, 'stroke-dasharray', style.lineDash.join(',')); attr(svgEl, 'stroke-dashoffset', mathRound(style.lineDashOffset || 0)); } else { attr(svgEl, 'stroke-dasharray', ''); } // PENDING style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap); style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin); style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit); } else { attr(svgEl, 'stroke', NONE); } } /*************************************************** * PATH **************************************************/ function pathDataToString$1(path) { var str = []; var data = path.data; var dataLength = path.len(); for (var i = 0; i < dataLength;) { var cmd = data[i++]; var cmdStr = ''; var nData = 0; switch (cmd) { case CMD$4.M: cmdStr = 'M'; nData = 2; break; case CMD$4.L: cmdStr = 'L'; nData = 2; break; case CMD$4.Q: cmdStr = 'Q'; nData = 4; break; case CMD$4.C: cmdStr = 'C'; nData = 6; break; case CMD$4.A: var cx = data[i++]; var cy = data[i++]; var rx = data[i++]; var ry = data[i++]; var theta = data[i++]; var dTheta = data[i++]; var psi = data[i++]; var clockwise = data[i++]; var dThetaPositive = Math.abs(dTheta); var isCircle = isAroundZero$1(dThetaPositive - PI2$7) && !isAroundZero$1(dThetaPositive); var large = false; if (dThetaPositive >= PI2$7) { large = true; } else if (isAroundZero$1(dThetaPositive)) { large = false; } else { large = (dTheta > -PI$5 && dTheta < 0 || dTheta > PI$5) === !!clockwise; } var x0 = round4(cx + rx * mathCos$3(theta)); var y0 = round4(cy + ry * mathSin$3(theta)); // It will not draw if start point and end point are exactly the same // We need to shift the end point with a small value // FIXME A better way to draw circle ? if (isCircle) { if (clockwise) { dTheta = PI2$7 - 1e-4; } else { dTheta = -PI2$7 + 1e-4; } large = true; if (i === 9) { // Move to (x0, y0) only when CMD.A comes at the // first position of a shape. // For instance, when drawing a ring, CMD.A comes // after CMD.M, so it's unnecessary to move to // (x0, y0). str.push('M', x0, y0); } } var x = round4(cx + rx * mathCos$3(theta + dTheta)); var y = round4(cy + ry * mathSin$3(theta + dTheta)); // FIXME Ellipse str.push('A', round4(rx), round4(ry), mathRound(psi * degree), +large, +clockwise, x, y); break; case CMD$4.Z: cmdStr = 'Z'; break; case CMD$4.R: var x = round4(data[i++]); var y = round4(data[i++]); var w = round4(data[i++]); var h = round4(data[i++]); str.push( 'M', x, y, 'L', x + w, y, 'L', x + w, y + h, 'L', x, y + h, 'L', x, y ); break; } cmdStr && str.push(cmdStr); for (var j = 0; j < nData; j++) { // PENDING With scale str.push(round4(data[i++])); } } return str.join(' '); } var svgPath = {}; svgPath.brush = function (el) { var style = el.style; var svgEl = el.__svgEl; if (!svgEl) { svgEl = createElement('path'); el.__svgEl = svgEl; } if (!el.path) { el.createPathProxy(); } var path = el.path; if (el.__dirtyPath) { path.beginPath(); el.buildPath(path, el.shape); el.__dirtyPath = false; var pathStr = pathDataToString$1(path); if (pathStr.indexOf('NaN') < 0) { // Ignore illegal path, which may happen such in out-of-range // data in Calendar series. attr(svgEl, 'd', pathStr); } } bindStyle(svgEl, style); setTransform(svgEl, el.transform); if (style.text != null) { svgTextDrawRectText(el, el.getBoundingRect()); } }; /*************************************************** * IMAGE **************************************************/ var svgImage = {}; svgImage.brush = function (el) { var style = el.style; var image = style.image; if (image instanceof HTMLImageElement) { var src = image.src; image = src; } if (! image) { return; } var x = style.x || 0; var y = style.y || 0; var dw = style.width; var dh = style.height; var svgEl = el.__svgEl; if (! svgEl) { svgEl = createElement('image'); el.__svgEl = svgEl; } if (image !== el.__imageSrc) { attrXLink(svgEl, 'href', image); // Caching image src el.__imageSrc = image; } attr(svgEl, 'width', dw); attr(svgEl, 'height', dh); attr(svgEl, 'x', x); attr(svgEl, 'y', y); setTransform(svgEl, el.transform); if (style.text != null) { svgTextDrawRectText(el, el.getBoundingRect()); } }; /*************************************************** * TEXT **************************************************/ var svgText = {}; var tmpRect$3 = new BoundingRect(); var svgTextDrawRectText = function (el, rect, textRect) { var style = el.style; el.__dirty && normalizeTextStyle(style, true); var text = style.text; // Convert to string if (text == null) { // Draw no text only when text is set to null, but not '' return; } else { text += ''; } var textSvgEl = el.__textSvgEl; if (! textSvgEl) { textSvgEl = createElement('text'); el.__textSvgEl = textSvgEl; } var x; var y; var textPosition = style.textPosition; var distance = style.textDistance; var align = style.textAlign || 'left'; if (typeof style.fontSize === 'number') { style.fontSize += 'px'; } var font = style.font || [ style.fontStyle || '', style.fontWeight || '', style.fontSize || '', style.fontFamily || '' ].join(' ') || DEFAULT_FONT; var verticalAlign = getVerticalAlignForSvg(style.textVerticalAlign); textRect = getBoundingRect(text, font, align, verticalAlign); var lineHeight = textRect.lineHeight; // Text position represented by coord if (textPosition instanceof Array) { x = rect.x + textPosition[0]; y = rect.y + textPosition[1]; } else { var newPos = adjustTextPositionOnRect( textPosition, rect, distance ); x = newPos.x; y = newPos.y; verticalAlign = getVerticalAlignForSvg(newPos.textVerticalAlign); align = newPos.textAlign; } attr(textSvgEl, 'alignment-baseline', verticalAlign); if (font) { textSvgEl.style.font = font; } var textPadding = style.textPadding; // Make baseline top attr(textSvgEl, 'x', x); attr(textSvgEl, 'y', y); bindStyle(textSvgEl, style, true); if (el instanceof Text || el.style.transformText) { // Transform text with element setTransform(textSvgEl, el.transform); } else { if (el.transform) { tmpRect$3.copy(rect); tmpRect$3.applyTransform(el.transform); rect = tmpRect$3; } else { var pos = el.transformCoordToGlobal(rect.x, rect.y); rect.x = pos[0]; rect.y = pos[1]; } // Text rotation, but no element transform var origin = style.textOrigin; if (origin === 'center') { x = textRect.width / 2 + x; y = textRect.height / 2 + y; } else if (origin) { x = origin[0] + x; y = origin[1] + y; } var rotate = -style.textRotation * 180 / Math.PI; attr(textSvgEl, 'transform', 'rotate(' + rotate + ',' + x + ',' + y + ')'); } var textLines = text.split('\n'); var nTextLines = textLines.length; var textAnchor = align; // PENDING if (textAnchor === 'left') { textAnchor = 'start'; textPadding && (x += textPadding[3]); } else if (textAnchor === 'right') { textAnchor = 'end'; textPadding && (x -= textPadding[1]); } else if (textAnchor === 'center') { textAnchor = 'middle'; textPadding && (x += (textPadding[3] - textPadding[1]) / 2); } var dy = 0; if (verticalAlign === 'baseline') { dy = -textRect.height + lineHeight; textPadding && (dy -= textPadding[2]); } else if (verticalAlign === 'middle') { dy = (-textRect.height + lineHeight) / 2; textPadding && (y += (textPadding[0] - textPadding[2]) / 2); } else { textPadding && (dy += textPadding[0]); } // Font may affect position of each tspan elements if (el.__text !== text || el.__textFont !== font) { var tspanList = el.__tspanList || []; el.__tspanList = tspanList; for (var i = 0; i < nTextLines; i++) { // Using cached tspan elements var tspan = tspanList[i]; if (! tspan) { tspan = tspanList[i] = createElement('tspan'); textSvgEl.appendChild(tspan); attr(tspan, 'alignment-baseline', verticalAlign); attr(tspan, 'text-anchor', textAnchor); } else { tspan.innerHTML = ''; } attr(tspan, 'x', x); attr(tspan, 'y', y + i * lineHeight + dy); tspan.appendChild(document.createTextNode(textLines[i])); } // Remove unsed tspan elements for (; i < tspanList.length; i++) { textSvgEl.removeChild(tspanList[i]); } tspanList.length = nTextLines; el.__text = text; el.__textFont = font; } else if (el.__tspanList.length) { // Update span x and y var len = el.__tspanList.length; for (var i = 0; i < len; ++i) { var tspan = el.__tspanList[i]; if (tspan) { attr(tspan, 'x', x); attr(tspan, 'y', y + i * lineHeight + dy); } } } }; function getVerticalAlignForSvg(verticalAlign) { if (verticalAlign === 'middle') { return 'middle'; } else if (verticalAlign === 'bottom') { return 'baseline'; } else { return 'hanging'; } } svgText.drawRectText = svgTextDrawRectText; svgText.brush = function (el) { var style = el.style; if (style.text != null) { // 强制设置 textPosition style.textPosition = [0, 0]; svgTextDrawRectText(el, { x: style.x || 0, y: style.y || 0, width: 0, height: 0 }, el.getBoundingRect()); } }; // Myers' Diff Algorithm // Modified from https://github.com/kpdecker/jsdiff/blob/master/src/diff/base.js function Diff() {} Diff.prototype = { diff: function (oldArr, newArr, equals) { if (!equals) { equals = function (a, b) { return a === b; }; } this.equals = equals; var self = this; oldArr = oldArr.slice(); newArr = newArr.slice(); // Allow subclasses to massage the input prior to running var newLen = newArr.length; var oldLen = oldArr.length; var editLength = 1; var maxEditLength = newLen + oldLen; var bestPath = [{ newPos: -1, components: [] }]; // Seed editLength = 0, i.e. the content starts with the same values var oldPos = this.extractCommon(bestPath[0], newArr, oldArr, 0); if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) { var indices = []; for (var i = 0; i < newArr.length; i++) { indices.push(i); } // Identity per the equality and tokenizer return [{ indices: indices, count: newArr.length }]; } // Main worker method. checks all permutations of a given edit length for acceptance. function execEditLength() { for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) { var basePath; var addPath = bestPath[diagonalPath - 1]; var removePath = bestPath[diagonalPath + 1]; var oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; if (addPath) { // No one else is going to attempt to use this value, clear it bestPath[diagonalPath - 1] = undefined; } var canAdd = addPath && addPath.newPos + 1 < newLen; var canRemove = removePath && 0 <= oldPos && oldPos < oldLen; if (!canAdd && !canRemove) { // If this path is a terminal then prune bestPath[diagonalPath] = undefined; continue; } // Select the diagonal that we want to branch from. We select the prior // path whose position in the new string is the farthest from the origin // and does not pass the bounds of the diff graph if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { basePath = clonePath(removePath); self.pushComponent(basePath.components, undefined, true); } else { basePath = addPath; // No need to clone, we've pulled it from the list basePath.newPos++; self.pushComponent(basePath.components, true, undefined); } oldPos = self.extractCommon(basePath, newArr, oldArr, diagonalPath); // If we have hit the end of both strings, then we are done if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) { return buildValues(self, basePath.components, newArr, oldArr); } else { // Otherwise track this path as a potential candidate and continue. bestPath[diagonalPath] = basePath; } } editLength++; } while (editLength <= maxEditLength) { var ret = execEditLength(); if (ret) { return ret; } } }, pushComponent: function (components, added, removed) { var last = components[components.length - 1]; if (last && last.added === added && last.removed === removed) { // We need to clone here as the component clone operation is just // as shallow array clone components[components.length - 1] = {count: last.count + 1, added: added, removed: removed }; } else { components.push({count: 1, added: added, removed: removed }); } }, extractCommon: function (basePath, newArr, oldArr, diagonalPath) { var newLen = newArr.length; var oldLen = oldArr.length; var newPos = basePath.newPos; var oldPos = newPos - diagonalPath; var commonCount = 0; while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newArr[newPos + 1], oldArr[oldPos + 1])) { newPos++; oldPos++; commonCount++; } if (commonCount) { basePath.components.push({count: commonCount}); } basePath.newPos = newPos; return oldPos; }, tokenize: function (value) { return value.slice(); }, join: function (value) { return value.slice(); } }; function buildValues(diff, components, newArr, oldArr) { var componentPos = 0; var componentLen = components.length; var newPos = 0; var oldPos = 0; for (; componentPos < componentLen; componentPos++) { var component = components[componentPos]; if (!component.removed) { var indices = []; for (var i = newPos; i < newPos + component.count; i++) { indices.push(i); } component.indices = indices; newPos += component.count; // Common case if (!component.added) { oldPos += component.count; } } else { var indices = []; for (var i = oldPos; i < oldPos + component.count; i++) { indices.push(i); } component.indices = indices; oldPos += component.count; } } return components; } function clonePath(path) { return { newPos: path.newPos, components: path.components.slice(0) }; } var arrayDiff = new Diff(); var arrayDiff$1 = function (oldArr, newArr, callback) { return arrayDiff.diff(oldArr, newArr, callback); }; /** * @file Manages elements that can be defined in <defs> in SVG, * e.g., gradients, clip path, etc. * @author Zhang Wenli */ var MARK_UNUSED = '0'; var MARK_USED = '1'; /** * Manages elements that can be defined in <defs> in SVG, * e.g., gradients, clip path, etc. * * @class * @param {number} zrId zrender instance id * @param {SVGElement} svgRoot root of SVG document * @param {string|string[]} tagNames possible tag names * @param {string} markLabel label name to make if the element * is used */ function Definable( zrId, svgRoot, tagNames, markLabel, domName ) { this._zrId = zrId; this._svgRoot = svgRoot; this._tagNames = typeof tagNames === 'string' ? [tagNames] : tagNames; this._markLabel = markLabel; this._domName = domName || '_dom'; this.nextId = 0; } Definable.prototype.createElement = createElement; /** * Get the <defs> tag for svgRoot; optionally creates one if not exists. * * @param {boolean} isForceCreating if need to create when not exists * @return {SVGDefsElement} SVG <defs> element, null if it doesn't * exist and isForceCreating is false */ Definable.prototype.getDefs = function (isForceCreating) { var svgRoot = this._svgRoot; var defs = this._svgRoot.getElementsByTagName('defs'); if (defs.length === 0) { // Not exist if (isForceCreating) { defs = svgRoot.insertBefore( this.createElement('defs'), // Create new tag svgRoot.firstChild // Insert in the front of svg ); if (!defs.contains) { // IE doesn't support contains method defs.contains = function (el) { var children = defs.children; if (!children) { return false; } for (var i = children.length - 1; i >= 0; --i) { if (children[i] === el) { return true; } } return false; }; } return defs; } else { return null; } } else { return defs[0]; } }; /** * Update DOM element if necessary. * * @param {Object|string} element style element. e.g., for gradient, * it may be '#ccc' or {type: 'linear', ...} * @param {Function|undefined} onUpdate update callback */ Definable.prototype.update = function (element, onUpdate) { if (!element) { return; } var defs = this.getDefs(false); if (element[this._domName] && defs.contains(element[this._domName])) { // Update DOM if (typeof onUpdate === 'function') { onUpdate(element); } } else { // No previous dom, create new var dom = this.add(element); if (dom) { element[this._domName] = dom; } } }; /** * Add gradient dom to defs * * @param {SVGElement} dom DOM to be added to <defs> */ Definable.prototype.addDom = function (dom) { var defs = this.getDefs(true); defs.appendChild(dom); }; /** * Remove DOM of a given element. * * @param {SVGElement} element element to remove dom */ Definable.prototype.removeDom = function (element) { var defs = this.getDefs(false); if (defs && element[this._domName]) { defs.removeChild(element[this._domName]); element[this._domName] = null; } }; /** * Get DOMs of this element. * * @return {HTMLDomElement} doms of this defineable elements in <defs> */ Definable.prototype.getDoms = function () { var defs = this.getDefs(false); if (!defs) { // No dom when defs is not defined return []; } var doms = []; each$1(this._tagNames, function (tagName) { var tags = defs.getElementsByTagName(tagName); // Note that tags is HTMLCollection, which is array-like // rather than real array. // So `doms.concat(tags)` add tags as one object. doms = doms.concat([].slice.call(tags)); }); return doms; }; /** * Mark DOMs to be unused before painting, and clear unused ones at the end * of the painting. */ Definable.prototype.markAllUnused = function () { var doms = this.getDoms(); var that = this; each$1(doms, function (dom) { dom[that._markLabel] = MARK_UNUSED; }); }; /** * Mark a single DOM to be used. * * @param {SVGElement} dom DOM to mark */ Definable.prototype.markUsed = function (dom) { if (dom) { dom[this._markLabel] = MARK_USED; } }; /** * Remove unused DOMs defined in <defs> */ Definable.prototype.removeUnused = function () { var defs = this.getDefs(false); if (!defs) { // Nothing to remove return; } var doms = this.getDoms(); var that = this; each$1(doms, function (dom) { if (dom[that._markLabel] !== MARK_USED) { // Remove gradient defs.removeChild(dom); } }); }; /** * Get SVG proxy. * * @param {Displayable} displayable displayable element * @return {Path|Image|Text} svg proxy of given element */ Definable.prototype.getSvgProxy = function (displayable) { if (displayable instanceof Path) { return svgPath; } else if (displayable instanceof ZImage) { return svgImage; } else if (displayable instanceof Text) { return svgText; } else { return svgPath; } }; /** * Get text SVG element. * * @param {Displayable} displayable displayable element * @return {SVGElement} SVG element of text */ Definable.prototype.getTextSvgElement = function (displayable) { return displayable.__textSvgEl; }; /** * Get SVG element. * * @param {Displayable} displayable displayable element * @return {SVGElement} SVG element */ Definable.prototype.getSvgElement = function (displayable) { return displayable.__svgEl; }; /** * @file Manages SVG gradient elements. * @author Zhang Wenli */ /** * Manages SVG gradient elements. * * @class * @extends Definable * @param {number} zrId zrender instance id * @param {SVGElement} svgRoot root of SVG document */ function GradientManager(zrId, svgRoot) { Definable.call( this, zrId, svgRoot, ['linearGradient', 'radialGradient'], '__gradient_in_use__' ); } inherits(GradientManager, Definable); /** * Create new gradient DOM for fill or stroke if not exist, * but will not update gradient if exists. * * @param {SvgElement} svgElement SVG element to paint * @param {Displayable} displayable zrender displayable element */ GradientManager.prototype.addWithoutUpdate = function ( svgElement, displayable ) { if (displayable && displayable.style) { var that = this; each$1(['fill', 'stroke'], function (fillOrStroke) { if (displayable.style[fillOrStroke] && (displayable.style[fillOrStroke].type === 'linear' || displayable.style[fillOrStroke].type === 'radial') ) { var gradient = displayable.style[fillOrStroke]; var defs = that.getDefs(true); // Create dom in <defs> if not exists var dom; if (gradient._dom) { // Gradient exists dom = gradient._dom; if (!defs.contains(gradient._dom)) { // _dom is no longer in defs, recreate that.addDom(dom); } } else { // New dom dom = that.add(gradient); } that.markUsed(displayable); var id = dom.getAttribute('id'); svgElement.setAttribute(fillOrStroke, 'url(#' + id + ')'); } }); } }; /** * Add a new gradient tag in <defs> * * @param {Gradient} gradient zr gradient instance * @return {SVGLinearGradientElement | SVGRadialGradientElement} * created DOM */ GradientManager.prototype.add = function (gradient) { var dom; if (gradient.type === 'linear') { dom = this.createElement('linearGradient'); } else if (gradient.type === 'radial') { dom = this.createElement('radialGradient'); } else { zrLog('Illegal gradient type.'); return null; } // Set dom id with gradient id, since each gradient instance // will have no more than one dom element. // id may exists before for those dirty elements, in which case // id should remain the same, and other attributes should be // updated. gradient.id = gradient.id || this.nextId++; dom.setAttribute('id', 'zr' + this._zrId + '-gradient-' + gradient.id); this.updateDom(gradient, dom); this.addDom(dom); return dom; }; /** * Update gradient. * * @param {Gradient} gradient zr gradient instance */ GradientManager.prototype.update = function (gradient) { var that = this; Definable.prototype.update.call(this, gradient, function () { var type = gradient.type; var tagName = gradient._dom.tagName; if (type === 'linear' && tagName === 'linearGradient' || type === 'radial' && tagName === 'radialGradient' ) { // Gradient type is not changed, update gradient that.updateDom(gradient, gradient._dom); } else { // Remove and re-create if type is changed that.removeDom(gradient); that.add(gradient); } }); }; /** * Update gradient dom * * @param {Gradient} gradient zr gradient instance * @param {SVGLinearGradientElement | SVGRadialGradientElement} dom * DOM to update */ GradientManager.prototype.updateDom = function (gradient, dom) { if (gradient.type === 'linear') { dom.setAttribute('x1', gradient.x); dom.setAttribute('y1', gradient.y); dom.setAttribute('x2', gradient.x2); dom.setAttribute('y2', gradient.y2); } else if (gradient.type === 'radial') { dom.setAttribute('cx', gradient.x); dom.setAttribute('cy', gradient.y); dom.setAttribute('r', gradient.r); } else { zrLog('Illegal gradient type.'); return; } if (gradient.global) { // x1, x2, y1, y2 in range of 0 to canvas width or height dom.setAttribute('gradientUnits', 'userSpaceOnUse'); } else { // x1, x2, y1, y2 in range of 0 to 1 dom.setAttribute('gradientUnits', 'objectBoundingBox'); } // Remove color stops if exists dom.innerHTML = ''; // Add color stops var colors = gradient.colorStops; for (var i = 0, len = colors.length; i < len; ++i) { var stop = this.createElement('stop'); stop.setAttribute('offset', colors[i].offset * 100 + '%'); stop.setAttribute('stop-color', colors[i].color); dom.appendChild(stop); } // Store dom element in gradient, to avoid creating multiple // dom instances for the same gradient element gradient._dom = dom; }; /** * Mark a single gradient to be used * * @param {Displayable} displayable displayable element */ GradientManager.prototype.markUsed = function (displayable) { if (displayable.style) { var gradient = displayable.style.fill; if (gradient && gradient._dom) { Definable.prototype.markUsed.call(this, gradient._dom); } gradient = displayable.style.stroke; if (gradient && gradient._dom) { Definable.prototype.markUsed.call(this, gradient._dom); } } }; /** * @file Manages SVG clipPath elements. * @author Zhang Wenli */ /** * Manages SVG clipPath elements. * * @class * @extends Definable * @param {number} zrId zrender instance id * @param {SVGElement} svgRoot root of SVG document */ function ClippathManager(zrId, svgRoot) { Definable.call(this, zrId, svgRoot, 'clipPath', '__clippath_in_use__'); } inherits(ClippathManager, Definable); /** * Update clipPath. * * @param {Displayable} displayable displayable element */ ClippathManager.prototype.update = function (displayable) { var svgEl = this.getSvgElement(displayable); if (svgEl) { this.updateDom(svgEl, displayable.__clipPaths, false); } var textEl = this.getTextSvgElement(displayable); if (textEl) { // Make another clipPath for text, since it's transform // matrix is not the same with svgElement this.updateDom(textEl, displayable.__clipPaths, true); } this.markUsed(displayable); }; /** * Create an SVGElement of displayable and create a <clipPath> of its * clipPath * * @param {Displayable} parentEl parent element * @param {ClipPath[]} clipPaths clipPaths of parent element * @param {boolean} isText if parent element is Text */ ClippathManager.prototype.updateDom = function ( parentEl, clipPaths, isText ) { if (clipPaths && clipPaths.length > 0) { // Has clipPath, create <clipPath> with the first clipPath var defs = this.getDefs(true); var clipPath = clipPaths[0]; var clipPathEl; var id; var dom = isText ? '_textDom' : '_dom'; if (clipPath[dom]) { // Use a dom that is already in <defs> id = clipPath[dom].getAttribute('id'); clipPathEl = clipPath[dom]; // Use a dom that is already in <defs> if (!defs.contains(clipPathEl)) { // This happens when set old clipPath that has // been previously removed defs.appendChild(clipPathEl); } } else { // New <clipPath> id = 'zr' + this._zrId + '-clip-' + this.nextId; ++this.nextId; clipPathEl = this.createElement('clipPath'); clipPathEl.setAttribute('id', id); defs.appendChild(clipPathEl); clipPath[dom] = clipPathEl; } // Build path and add to <clipPath> var svgProxy = this.getSvgProxy(clipPath); if (clipPath.transform && clipPath.parent.invTransform && !isText ) { /** * If a clipPath has a parent with transform, the transform * of parent should not be considered when setting transform * of clipPath. So we need to transform back from parent's * transform, which is done by multiplying parent's inverse * transform. */ // Store old transform var transform = Array.prototype.slice.call( clipPath.transform ); // Transform back from parent, and brush path mul$1( clipPath.transform, clipPath.parent.invTransform, clipPath.transform ); svgProxy.brush(clipPath); // Set back transform of clipPath clipPath.transform = transform; } else { svgProxy.brush(clipPath); } var pathEl = this.getSvgElement(clipPath); clipPathEl.innerHTML = ''; /** * Use `cloneNode()` here to appendChild to multiple parents, * which may happend when Text and other shapes are using the same * clipPath. Since Text will create an extra clipPath DOM due to * different transform rules. */ clipPathEl.appendChild(pathEl.cloneNode()); parentEl.setAttribute('clip-path', 'url(#' + id + ')'); if (clipPaths.length > 1) { // Make the other clipPaths recursively this.updateDom(clipPathEl, clipPaths.slice(1), isText); } } else { // No clipPath if (parentEl) { parentEl.setAttribute('clip-path', 'none'); } } }; /** * Mark a single clipPath to be used * * @param {Displayable} displayable displayable element */ ClippathManager.prototype.markUsed = function (displayable) { var that = this; if (displayable.__clipPaths && displayable.__clipPaths.length > 0) { each$1(displayable.__clipPaths, function (clipPath) { if (clipPath._dom) { Definable.prototype.markUsed.call(that, clipPath._dom); } if (clipPath._textDom) { Definable.prototype.markUsed.call(that, clipPath._textDom); } }); } }; /** * @file Manages SVG shadow elements. * @author Zhang Wenli */ /** * Manages SVG shadow elements. * * @class * @extends Definable * @param {number} zrId zrender instance id * @param {SVGElement} svgRoot root of SVG document */ function ShadowManager(zrId, svgRoot) { Definable.call( this, zrId, svgRoot, ['filter'], '__filter_in_use__', '_shadowDom' ); } inherits(ShadowManager, Definable); /** * Create new shadow DOM for fill or stroke if not exist, * but will not update shadow if exists. * * @param {SvgElement} svgElement SVG element to paint * @param {Displayable} displayable zrender displayable element */ ShadowManager.prototype.addWithoutUpdate = function ( svgElement, displayable ) { if (displayable && hasShadow(displayable.style)) { var style = displayable.style; // Create dom in <defs> if not exists var dom; if (style._shadowDom) { // Gradient exists dom = style._shadowDom; var defs = this.getDefs(true); if (!defs.contains(style._shadowDom)) { // _shadowDom is no longer in defs, recreate this.addDom(dom); } } else { // New dom dom = this.add(displayable); } this.markUsed(displayable); var id = dom.getAttribute('id'); svgElement.style.filter = 'url(#' + id + ')'; } }; /** * Add a new shadow tag in <defs> * * @param {Displayable} displayable zrender displayable element * @return {SVGFilterElement} created DOM */ ShadowManager.prototype.add = function (displayable) { var dom = this.createElement('filter'); var style = displayable.style; // Set dom id with shadow id, since each shadow instance // will have no more than one dom element. // id may exists before for those dirty elements, in which case // id should remain the same, and other attributes should be // updated. style._shadowDomId = style._shadowDomId || this.nextId++; dom.setAttribute('id', 'zr' + this._zrId + '-shadow-' + style._shadowDomId); this.updateDom(displayable, dom); this.addDom(dom); return dom; }; /** * Update shadow. * * @param {Displayable} displayable zrender displayable element */ ShadowManager.prototype.update = function (svgElement, displayable) { var style = displayable.style; if (hasShadow(style)) { var that = this; Definable.prototype.update.call(this, displayable, function (style) { that.updateDom(displayable, style._shadowDom); }); } else { // Remove shadow this.remove(svgElement, style); } }; /** * Remove DOM and clear parent filter */ ShadowManager.prototype.remove = function (svgElement, style) { if (style._shadowDomId != null) { this.removeDom(style); svgElement.style.filter = ''; } }; /** * Update shadow dom * * @param {Displayable} displayable zrender displayable element * @param {SVGFilterElement} dom DOM to update */ ShadowManager.prototype.updateDom = function (displayable, dom) { var domChild = dom.getElementsByTagName('feDropShadow'); if (domChild.length === 0) { domChild = this.createElement('feDropShadow'); } else { domChild = domChild[0]; } var style = displayable.style; var scaleX = displayable.scale ? (displayable.scale[0] || 1) : 1; var scaleY = displayable.scale ? (displayable.scale[1] || 1) : 1; // TODO: textBoxShadowBlur is not supported yet var offsetX, offsetY, blur, color; if (style.shadowBlur || style.shadowOffsetX || style.shadowOffsetY) { offsetX = style.shadowOffsetX || 0; offsetY = style.shadowOffsetY || 0; blur = style.shadowBlur; color = style.shadowColor; } else if (style.textShadowBlur) { offsetX = style.textShadowOffsetX || 0; offsetY = style.textShadowOffsetY || 0; blur = style.textShadowBlur; color = style.textShadowColor; } else { // Remove shadow this.removeDom(dom, style); return; } domChild.setAttribute('dx', offsetX / scaleX); domChild.setAttribute('dy', offsetY / scaleY); domChild.setAttribute('flood-color', color); // Divide by two here so that it looks the same as in canvas // See: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowblur var stdDx = blur / 2 / scaleX; var stdDy = blur / 2 / scaleY; var stdDeviation = stdDx + ' ' + stdDy; domChild.setAttribute('stdDeviation', stdDeviation); // Fix filter clipping problem dom.setAttribute('x', '-100%'); dom.setAttribute('y', '-100%'); dom.setAttribute('width', Math.ceil(blur / 2 * 200) + '%'); dom.setAttribute('height', Math.ceil(blur / 2 * 200) + '%'); dom.appendChild(domChild); // Store dom element in shadow, to avoid creating multiple // dom instances for the same shadow element style._shadowDom = dom; }; /** * Mark a single shadow to be used * * @param {Displayable} displayable displayable element */ ShadowManager.prototype.markUsed = function (displayable) { var style = displayable.style; if (style && style._shadowDom) { Definable.prototype.markUsed.call(this, style._shadowDom); } }; function hasShadow(style) { // TODO: textBoxShadowBlur is not supported yet return style && (style.shadowBlur || style.shadowOffsetX || style.shadowOffsetY || style.textShadowBlur || style.textShadowOffsetX || style.textShadowOffsetY); } /** * SVG Painter * @module zrender/svg/Painter */ function parseInt10$2(val) { return parseInt(val, 10); } function getSvgProxy(el) { if (el instanceof Path) { return svgPath; } else if (el instanceof ZImage) { return svgImage; } else if (el instanceof Text) { return svgText; } else { return svgPath; } } function checkParentAvailable(parent, child) { return child && parent && child.parentNode !== parent; } function insertAfter(parent, child, prevSibling) { if (checkParentAvailable(parent, child) && prevSibling) { var nextSibling = prevSibling.nextSibling; nextSibling ? parent.insertBefore(child, nextSibling) : parent.appendChild(child); } } function prepend(parent, child) { if (checkParentAvailable(parent, child)) { var firstChild = parent.firstChild; firstChild ? parent.insertBefore(child, firstChild) : parent.appendChild(child); } } function remove$1(parent, child) { if (child && parent && child.parentNode === parent) { parent.removeChild(child); } } function getTextSvgElement(displayable) { return displayable.__textSvgEl; } function getSvgElement(displayable) { return displayable.__svgEl; } /** * @alias module:zrender/svg/Painter * @constructor * @param {HTMLElement} root 绘图容器 * @param {module:zrender/Storage} storage * @param {Object} opts */ var SVGPainter = function (root, storage, opts, zrId) { this.root = root; this.storage = storage; this._opts = opts = extend({}, opts || {}); var svgRoot = createElement('svg'); svgRoot.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svgRoot.setAttribute('version', '1.1'); svgRoot.setAttribute('baseProfile', 'full'); svgRoot.style.cssText = 'user-select:none;position:absolute;left:0;top:0;'; this.gradientManager = new GradientManager(zrId, svgRoot); this.clipPathManager = new ClippathManager(zrId, svgRoot); this.shadowManager = new ShadowManager(zrId, svgRoot); var viewport = document.createElement('div'); viewport.style.cssText = 'overflow:hidden;position:relative'; this._svgRoot = svgRoot; this._viewport = viewport; root.appendChild(viewport); viewport.appendChild(svgRoot); this.resize(opts.width, opts.height); this._visibleList = []; }; SVGPainter.prototype = { constructor: SVGPainter, getType: function () { return 'svg'; }, getViewportRoot: function () { return this._viewport; }, getViewportRootOffset: function () { var viewportRoot = this.getViewportRoot(); if (viewportRoot) { return { offsetLeft: viewportRoot.offsetLeft || 0, offsetTop: viewportRoot.offsetTop || 0 }; } }, refresh: function () { var list = this.storage.getDisplayList(true); this._paintList(list); }, setBackgroundColor: function (backgroundColor) { // TODO gradient this._viewport.style.background = backgroundColor; }, _paintList: function (list) { this.gradientManager.markAllUnused(); this.clipPathManager.markAllUnused(); this.shadowManager.markAllUnused(); var svgRoot = this._svgRoot; var visibleList = this._visibleList; var listLen = list.length; var newVisibleList = []; var i; for (i = 0; i < listLen; i++) { var displayable = list[i]; var svgProxy = getSvgProxy(displayable); var svgElement = getSvgElement(displayable) || getTextSvgElement(displayable); if (!displayable.invisible) { if (displayable.__dirty) { svgProxy && svgProxy.brush(displayable); // Update clipPath this.clipPathManager.update(displayable); // Update gradient and shadow if (displayable.style) { this.gradientManager .update(displayable.style.fill); this.gradientManager .update(displayable.style.stroke); this.shadowManager .update(svgElement, displayable); } displayable.__dirty = false; } newVisibleList.push(displayable); } } var diff = arrayDiff$1(visibleList, newVisibleList); var prevSvgElement; // First do remove, in case element moved to the head and do remove // after add for (i = 0; i < diff.length; i++) { var item = diff[i]; if (item.removed) { for (var k = 0; k < item.count; k++) { var displayable = visibleList[item.indices[k]]; var svgElement = getSvgElement(displayable); var textSvgElement = getTextSvgElement(displayable); remove$1(svgRoot, svgElement); remove$1(svgRoot, textSvgElement); } } } for (i = 0; i < diff.length; i++) { var item = diff[i]; if (item.added) { for (var k = 0; k < item.count; k++) { var displayable = newVisibleList[item.indices[k]]; var svgElement = getSvgElement(displayable); var textSvgElement = getTextSvgElement(displayable); prevSvgElement ? insertAfter(svgRoot, svgElement, prevSvgElement) : prepend(svgRoot, svgElement); if (svgElement) { insertAfter(svgRoot, textSvgElement, svgElement); } else if (prevSvgElement) { insertAfter( svgRoot, textSvgElement, prevSvgElement ); } else { prepend(svgRoot, textSvgElement); } // Insert text insertAfter(svgRoot, textSvgElement, svgElement); prevSvgElement = textSvgElement || svgElement || prevSvgElement; this.gradientManager .addWithoutUpdate(svgElement, displayable); this.shadowManager .addWithoutUpdate(prevSvgElement, displayable); this.clipPathManager.markUsed(displayable); } } else if (!item.removed) { for (var k = 0; k < item.count; k++) { var displayable = newVisibleList[item.indices[k]]; prevSvgElement = svgElement = getTextSvgElement(displayable) || getSvgElement(displayable) || prevSvgElement; this.gradientManager.markUsed(displayable); this.gradientManager .addWithoutUpdate(svgElement, displayable); this.shadowManager.markUsed(displayable); this.shadowManager .addWithoutUpdate(svgElement, displayable); this.clipPathManager.markUsed(displayable); } } } this.gradientManager.removeUnused(); this.clipPathManager.removeUnused(); this.shadowManager.removeUnused(); this._visibleList = newVisibleList; }, _getDefs: function (isForceCreating) { var svgRoot = this._svgRoot; var defs = this._svgRoot.getElementsByTagName('defs'); if (defs.length === 0) { // Not exist if (isForceCreating) { var defs = svgRoot.insertBefore( createElement('defs'), // Create new tag svgRoot.firstChild // Insert in the front of svg ); if (!defs.contains) { // IE doesn't support contains method defs.contains = function (el) { var children = defs.children; if (!children) { return false; } for (var i = children.length - 1; i >= 0; --i) { if (children[i] === el) { return true; } } return false; }; } return defs; } else { return null; } } else { return defs[0]; } }, resize: function (width, height) { var viewport = this._viewport; // FIXME Why ? viewport.style.display = 'none'; // Save input w/h var opts = this._opts; width != null && (opts.width = width); height != null && (opts.height = height); width = this._getSize(0); height = this._getSize(1); viewport.style.display = ''; if (this._width !== width || this._height !== height) { this._width = width; this._height = height; var viewportStyle = viewport.style; viewportStyle.width = width + 'px'; viewportStyle.height = height + 'px'; var svgRoot = this._svgRoot; // Set width by 'svgRoot.width = width' is invalid svgRoot.setAttribute('width', width); svgRoot.setAttribute('height', height); } }, /** * 获取绘图区域宽度 */ getWidth: function () { return this._width; }, /** * 获取绘图区域高度 */ getHeight: function () { return this._height; }, _getSize: function (whIdx) { var opts = this._opts; var wh = ['width', 'height'][whIdx]; var cwh = ['clientWidth', 'clientHeight'][whIdx]; var plt = ['paddingLeft', 'paddingTop'][whIdx]; var prb = ['paddingRight', 'paddingBottom'][whIdx]; if (opts[wh] != null && opts[wh] !== 'auto') { return parseFloat(opts[wh]); } var root = this.root; // IE8 does not support getComputedStyle, but it use VML. var stl = document.defaultView.getComputedStyle(root); return ( (root[cwh] || parseInt10$2(stl[wh]) || parseInt10$2(root.style[wh])) - (parseInt10$2(stl[plt]) || 0) - (parseInt10$2(stl[prb]) || 0) ) | 0; }, dispose: function () { this.root.innerHTML = ''; this._svgRoot = this._viewport = this.storage = null; }, clear: function () { if (this._viewport) { this.root.removeChild(this._viewport); } }, pathToDataUrl: function () { this.refresh(); var html = this._svgRoot.outerHTML; return 'data:image/svg+xml;charset=UTF-8,' + html; } }; // Not supported methods function createMethodNotSupport$1(method) { return function () { zrLog('In SVG mode painter not support method "' + method + '"'); }; } // Unsuppoted methods each$1([ 'getLayer', 'insertLayer', 'eachLayer', 'eachBuiltinLayer', 'eachOtherLayer', 'getLayers', 'modLayer', 'delLayer', 'clearLayer', 'toDataURL', 'pathToImage' ], function (name) { SVGPainter.prototype[name] = createMethodNotSupport$1(name); }); registerPainter('svg', SVGPainter); // Import all charts and components exports.version = version; exports.dependencies = dependencies; exports.PRIORITY = PRIORITY; exports.init = init; exports.connect = connect; exports.disConnect = disConnect; exports.disconnect = disconnect; exports.dispose = dispose; exports.getInstanceByDom = getInstanceByDom; exports.getInstanceById = getInstanceById; exports.registerTheme = registerTheme; exports.registerPreprocessor = registerPreprocessor; exports.registerProcessor = registerProcessor; exports.registerPostUpdate = registerPostUpdate; exports.registerAction = registerAction; exports.registerCoordinateSystem = registerCoordinateSystem; exports.getCoordinateSystemDimensions = getCoordinateSystemDimensions; exports.registerLayout = registerLayout; exports.registerVisual = registerVisual; exports.registerLoading = registerLoading; exports.extendComponentModel = extendComponentModel; exports.extendComponentView = extendComponentView; exports.extendSeriesModel = extendSeriesModel; exports.extendChartView = extendChartView; exports.setCanvasCreator = setCanvasCreator; exports.registerMap = registerMap; exports.getMap = getMap; exports.dataTool = dataTool; exports.zrender = zrender; exports.graphic = graphic; exports.number = number; exports.format = format; exports.throttle = throttle; exports.helper = helper; exports.matrix = matrix; exports.vector = vector; exports.color = color; exports.parseGeoJSON = parseGeoJson$1; exports.parseGeoJson = parseGeoJson; exports.util = ecUtil; exports.List = List; exports.Model = Model; exports.Axis = Axis; exports.env = env$1; }))); //# sourceMappingURL=echarts.js.map Kodo/kodo - Gogs: Go Git Service

1 Commits (268aa4ebe3d4f2a8812fc8cd05abd10902763e27)

Autore SHA1 Messaggio Data
  Brightcells f6a88c632b add group_default_avatar field for GroupInfo 10 anni fa