| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import { __DEV__ } from '../config'; |
| import * as echarts from '../echarts'; |
| import * as zrUtil from 'zrender/src/core/util'; |
| import * as modelUtil from '../util/model'; |
| import * as graphicUtil from '../util/graphic'; |
| import * as layoutUtil from '../util/layout'; // ------------- |
| // Preprocessor |
| // ------------- |
| |
| echarts.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 (zrUtil.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 = echarts.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.) |
| // info: custom info. enables user to mount some info on elements and use them |
| // in event handlers. Update them only when user specified, otherwise, remain. |
| // 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 = modelUtil.mappingToExists(existList, flattenedList); |
| modelUtil.makeIdAndName(mappingResult); // Clear elOptionsToUpdate |
| |
| var elOptionsToUpdate = this._elOptionsToUpdate = []; |
| zrUtil.each(mappingResult, function (resultItem, index) { |
| var newElOption = resultItem.option; |
| |
| 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) { |
| zrUtil.each(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 |
| // ----- |
| |
| echarts.extendComponentView({ |
| type: 'graphic', |
| |
| /** |
| * @override |
| */ |
| init: function (ecModel, api) { |
| /** |
| * @private |
| * @type {module:zrender/core/util.HashMap} |
| */ |
| this._elMap = zrUtil.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); |
| |
| this._relocate(graphicModel, api); |
| }, |
| |
| /** |
| * Update graphic elements. |
| * |
| * @private |
| * @param {Object} graphicModel graphic model |
| */ |
| _updateElements: function (graphicModel) { |
| 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. |
| |
| zrUtil.each(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; |
| var elOptionStyle = elOption.style; |
| |
| if (elOption.type === 'text' && elOptionStyle) { |
| // 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 (!$action || $action === 'merge') { |
| existEl ? existEl.attr(elOptionCleaned) : createEl(id, targetElParent, elOptionCleaned, elMap); |
| } else if ($action === 'replace') { |
| removeEl(existEl, elMap); |
| createEl(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; |
| setEventData(el, graphicModel, elOption); |
| } |
| }); |
| }, |
| |
| /** |
| * 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 |
| }; |
| layoutUtil.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 = zrUtil.createHashMap(); |
| }, |
| |
| /** |
| * @override |
| */ |
| dispose: function () { |
| this._clear(); |
| } |
| }); |
| |
| function createEl(id, targetElParent, elOption, elMap) { |
| var graphicType = elOption.type; |
| var Clz = graphicUtil[graphicType.charAt(0).toUpperCase() + graphicType.slice(1)]; |
| 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 = zrUtil.extend({}, elOption); |
| zrUtil.each(['id', 'parentId', '$action', 'hv', 'bounding'].concat(layoutUtil.LOCATION_PARAMS), function (name) { |
| delete elOption[name]; |
| }); |
| return elOption; |
| } |
| |
| function isSetLoc(obj, props) { |
| var isSet; |
| zrUtil.each(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 = zrUtil.extend({}, newElOption); |
| var existElOption = existList[index]; |
| var $action = newElOption.$action || 'merge'; |
| |
| if ($action === 'merge') { |
| if (existElOption) { |
| // We can ensure that newElOptCopy and existElOption are not |
| // the same object, so `merge` will not change newElOptCopy. |
| zrUtil.merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize. |
| |
| layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, { |
| ignoreSize: true |
| }); // Will be used in render. |
| |
| layoutUtil.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); |
| } |
| } |
| |
| function setEventData(el, graphicModel, elOption) { |
| var eventData = el.eventData; // Simple optimize for large amount of elements that no need event. |
| |
| if (!el.silent && !el.ignore && !eventData) { |
| eventData = el.eventData = { |
| componentType: 'graphic', |
| componentIndex: graphicModel.componentIndex, |
| name: el.name |
| }; |
| } // `elOption.info` enables user to mount some info on |
| // elements and use them in event handlers. |
| |
| |
| if (eventData) { |
| eventData.info = el.info; |
| } |
| } |