/* * 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. */ var _config = require("../config"); var __DEV__ = _config.__DEV__; var echarts = require("../echarts"); var zrUtil = require("zrender/lib/core/util"); var modelUtil = require("../util/model"); var graphicUtil = require("../util/graphic"); var layoutUtil = require("../util/layout"); var _number = require("../util/number"); var parsePercent = _number.parsePercent; /* * 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. */ var _nonShapeGraphicElements = { // Reserved but not supported in graphic component. path: null, compoundPath: null, // Supported in graphic component. group: graphicUtil.Group, image: graphicUtil.Image, text: graphicUtil.Text }; // ------------- // 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.} */ _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.} optionList option list * @param {Array.} 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.__ecGraphicWidthOption = elOption.width; el.__ecGraphicHeightOption = 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; var apiWidth = api.getWidth(); var apiHeight = api.getHeight(); // Top-down to calculate percentage width/height of group for (var i = 0; i < elOptions.length; i++) { var elOption = elOptions[i]; var el = elMap.get(elOption.id); if (!el || !el.isGroup) { continue; } var parentEl = el.parent; var isParentRoot = parentEl === rootGroup; // Like 'position:absolut' in css, default 0. el.__ecGraphicWidth = parsePercent(el.__ecGraphicWidthOption, isParentRoot ? apiWidth : parentEl.__ecGraphicWidth) || 0; el.__ecGraphicHeight = parsePercent(el.__ecGraphicHeightOption, isParentRoot ? apiHeight : parentEl.__ecGraphicHeight) || 0; } // 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: apiWidth, height: apiHeight } : { width: parentEl.__ecGraphicWidth, height: parentEl.__ecGraphicHeight }; // PENDING // Currently, when `bounding: 'all'`, the union bounding rect of the group // does not include the rect of [0, 0, group.width, group.height], which // is probably weird for users. Should we make a break change for it? 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 = _nonShapeGraphicElements.hasOwnProperty(graphicType) // Those graphic elements are not shapes. They should not be // overwritten by users, so do them first. ? _nonShapeGraphicElements[graphicType] : graphicUtil.getShapeClass(graphicType); 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; } }