You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							504 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							504 lines
						
					
					
						
							16 KiB
						
					
					
				
 | 
						|
/* | 
						|
* 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"); | 
						|
 | 
						|
/* | 
						|
* 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. | 
						|
*/ | 
						|
// ------------- | 
						|
// 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; | 
						|
  } | 
						|
} |