blob: ea4f8f89bbbb2845e76a495b1ae481de019693c7 [file] [log] [blame]
/*
* 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 * as zrUtil from 'zrender/src/core/util';
import * as pathTool from 'zrender/src/tool/path';
import * as colorTool from 'zrender/src/tool/color';
import * as matrix from 'zrender/src/core/matrix';
import * as vector from 'zrender/src/core/vector';
import Path from 'zrender/src/graphic/Path';
import Transformable from 'zrender/src/mixin/Transformable';
import ZImage from 'zrender/src/graphic/Image';
import Group from 'zrender/src/container/Group';
import Text from 'zrender/src/graphic/Text';
import Circle from 'zrender/src/graphic/shape/Circle';
import Sector from 'zrender/src/graphic/shape/Sector';
import Ring from 'zrender/src/graphic/shape/Ring';
import Polygon from 'zrender/src/graphic/shape/Polygon';
import Polyline from 'zrender/src/graphic/shape/Polyline';
import Rect from 'zrender/src/graphic/shape/Rect';
import Line from 'zrender/src/graphic/shape/Line';
import BezierCurve from 'zrender/src/graphic/shape/BezierCurve';
import Arc from 'zrender/src/graphic/shape/Arc';
import CompoundPath from 'zrender/src/graphic/CompoundPath';
import LinearGradient from 'zrender/src/graphic/LinearGradient';
import RadialGradient from 'zrender/src/graphic/RadialGradient';
import BoundingRect from 'zrender/src/core/BoundingRect';
import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
var round = Math.round;
var mathMax = Math.max;
var mathMin = Math.min;
var EMPTY_OBJ = {};
/**
* Extend shape with parameters
*/
export function extendShape(opts) {
return Path.extend(opts);
}
/**
* Extend path
*/
export function extendPath(pathData, opts) {
return pathTool.extendFromString(pathData, opts);
}
/**
* Create a path element from path data string
* @param {string} pathData
* @param {Object} opts
* @param {module:zrender/core/BoundingRect} rect
* @param {string} [layout=cover] 'center' or 'cover'
*/
export function makePath(pathData, opts, rect, layout) {
var path = pathTool.createFromString(pathData, opts);
if (rect) {
if (layout === 'center') {
rect = centerGraphic(rect, path.getBoundingRect());
}
resizePath(path, rect);
}
return path;
}
/**
* Create a image element from image url
* @param {string} imageUrl image url
* @param {Object} opts options
* @param {module:zrender/core/BoundingRect} rect constrain rect
* @param {string} [layout=cover] 'center' or 'cover'
*/
export function makeImage(imageUrl, rect, layout) {
var path = new ZImage({
style: {
image: imageUrl,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
},
onload: function (img) {
if (layout === 'center') {
var boundingRect = {
width: img.width,
height: img.height
};
path.setStyle(centerGraphic(rect, boundingRect));
}
}
});
return path;
}
/**
* Get position of centered element in bounding box.
*
* @param {Object} rect element local bounding box
* @param {Object} boundingRect constraint bounding box
* @return {Object} element position containing x, y, width, and height
*/
function centerGraphic(rect, boundingRect) {
// Set rect to center, keep width / height ratio.
var aspect = boundingRect.width / boundingRect.height;
var width = rect.height * aspect;
var height;
if (width <= rect.width) {
height = rect.height;
} else {
width = rect.width;
height = width / aspect;
}
var cx = rect.x + rect.width / 2;
var cy = rect.y + rect.height / 2;
return {
x: cx - width / 2,
y: cy - height / 2,
width: width,
height: height
};
}
export var mergePath = pathTool.mergePath;
/**
* Resize a path to fit the rect
* @param {module:zrender/graphic/Path} path
* @param {Object} rect
*/
export function resizePath(path, rect) {
if (!path.applyTransform) {
return;
}
var pathRect = path.getBoundingRect();
var m = pathRect.calculateTransform(rect);
path.applyTransform(m);
}
/**
* Sub pixel optimize line for canvas
*
* @param {Object} param
* @param {Object} [param.shape]
* @param {number} [param.shape.x1]
* @param {number} [param.shape.y1]
* @param {number} [param.shape.x2]
* @param {number} [param.shape.y2]
* @param {Object} [param.style]
* @param {number} [param.style.lineWidth]
* @return {Object} Modified param
*/
export function subPixelOptimizeLine(param) {
var shape = param.shape;
var lineWidth = param.style.lineWidth;
if (round(shape.x1 * 2) === round(shape.x2 * 2)) {
shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true);
}
if (round(shape.y1 * 2) === round(shape.y2 * 2)) {
shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true);
}
return param;
}
/**
* Sub pixel optimize rect for canvas
*
* @param {Object} param
* @param {Object} [param.shape]
* @param {number} [param.shape.x]
* @param {number} [param.shape.y]
* @param {number} [param.shape.width]
* @param {number} [param.shape.height]
* @param {Object} [param.style]
* @param {number} [param.style.lineWidth]
* @return {Object} Modified param
*/
export function subPixelOptimizeRect(param) {
var shape = param.shape;
var lineWidth = param.style.lineWidth;
var originX = shape.x;
var originY = shape.y;
var originWidth = shape.width;
var originHeight = shape.height;
shape.x = subPixelOptimize(shape.x, lineWidth, true);
shape.y = subPixelOptimize(shape.y, lineWidth, true);
shape.width = Math.max(subPixelOptimize(originX + originWidth, lineWidth, false) - shape.x, originWidth === 0 ? 0 : 1);
shape.height = Math.max(subPixelOptimize(originY + originHeight, lineWidth, false) - shape.y, originHeight === 0 ? 0 : 1);
return param;
}
/**
* Sub pixel optimize for canvas
*
* @param {number} position Coordinate, such as x, y
* @param {number} lineWidth Should be nonnegative integer.
* @param {boolean=} positiveOrNegative Default false (negative).
* @return {number} Optimized position.
*/
export function subPixelOptimize(position, lineWidth, positiveOrNegative) {
// Assure that (position + lineWidth / 2) is near integer edge,
// otherwise line will be fuzzy in canvas.
var doubledPosition = round(position * 2);
return (doubledPosition + round(lineWidth)) % 2 === 0 ? doubledPosition / 2 : (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2;
}
function hasFillOrStroke(fillOrStroke) {
return fillOrStroke != null && fillOrStroke !== 'none';
} // Most lifted color are duplicated.
var liftedColorMap = zrUtil.createHashMap();
var liftedColorCount = 0;
function liftColor(color) {
if (typeof color !== 'string') {
return color;
}
var liftedColor = liftedColorMap.get(color);
if (!liftedColor) {
liftedColor = colorTool.lift(color, -0.1);
if (liftedColorCount < 10000) {
liftedColorMap.set(color, liftedColor);
liftedColorCount++;
}
}
return liftedColor;
}
function cacheElementStl(el) {
if (!el.__hoverStlDirty) {
return;
}
el.__hoverStlDirty = false;
var hoverStyle = el.__hoverStl;
if (!hoverStyle) {
el.__normalStl = null;
return;
}
var normalStyle = el.__normalStl = {};
var elStyle = el.style;
for (var name in hoverStyle) {
// See comment in `doSingleEnterHover`.
if (hoverStyle[name] != null) {
normalStyle[name] = elStyle[name];
}
} // Always cache fill and stroke to normalStyle for lifting color.
normalStyle.fill = elStyle.fill;
normalStyle.stroke = elStyle.stroke;
}
function doSingleEnterHover(el) {
var hoverStl = el.__hoverStl;
if (!hoverStl || el.__highlighted) {
return;
}
var useHoverLayer = el.useHoverLayer;
el.__highlighted = useHoverLayer ? 'layer' : 'plain';
var zr = el.__zr;
if (!zr && useHoverLayer) {
return;
}
var elTarget = el;
var targetStyle = el.style;
if (useHoverLayer) {
elTarget = zr.addHover(el);
targetStyle = elTarget.style;
} // Consider case: only `position: 'top'` is set on emphasis, then text
// color should be returned to `autoColor`, rather than remain '#fff'.
// So we should rollback then apply again after style merging.
rollbackDefaultTextStyle(targetStyle);
if (!useHoverLayer) {
cacheElementStl(elTarget);
} // styles can be:
// {
// label: {
// show: false,
// position: 'outside',
// fontSize: 18
// },
// emphasis: {
// label: {
// show: true
// }
// }
// },
// where properties of `emphasis` may not appear in `normal`. We previously use
// module:echarts/util/model#defaultEmphasis to merge `normal` to `emphasis`.
// But consider rich text and setOption in merge mode, it is impossible to cover
// all properties in merge. So we use merge mode when setting style here, where
// only properties that is not `null/undefined` can be set. The disadventage:
// null/undefined can not be used to remove style any more in `emphasis`.
targetStyle.extendFrom(hoverStl);
setDefaultHoverFillStroke(targetStyle, hoverStl, 'fill');
setDefaultHoverFillStroke(targetStyle, hoverStl, 'stroke');
applyDefaultTextStyle(targetStyle);
if (!useHoverLayer) {
el.dirty(false);
el.z2 += 1;
}
}
function setDefaultHoverFillStroke(targetStyle, hoverStyle, prop) {
if (!hasFillOrStroke(hoverStyle[prop]) && hasFillOrStroke(targetStyle[prop])) {
targetStyle[prop] = liftColor(targetStyle[prop]);
}
}
function doSingleLeaveHover(el) {
if (el.__highlighted) {
doSingleRestoreHoverStyle(el);
el.__highlighted = false;
}
}
function doSingleRestoreHoverStyle(el) {
var highlighted = el.__highlighted;
if (highlighted === 'layer') {
el.__zr && el.__zr.removeHover(el);
} else if (highlighted) {
var style = el.style;
var normalStl = el.__normalStl;
if (normalStl) {
rollbackDefaultTextStyle(style); // Consider null/undefined value, should use
// `setStyle` but not `extendFrom(stl, true)`.
el.setStyle(normalStl);
applyDefaultTextStyle(style);
el.z2 -= 1;
}
}
}
function traverseCall(el, method) {
el.isGroup ? el.traverse(function (child) {
!child.isGroup && method(child);
}) : method(el);
}
/**
* Set hover style of element.
*
* @param {module:zrender/Element} el Should not be `zrender/container/Group`.
* @param {Object|boolean} [hoverStl] The specified hover style.
* If set as `false`, disable the hover style.
* Similarly, The `el.hoverStyle` can alse be set
* as `false` to disable the hover style.
* Otherwise, use the default hover style if not provided.
* @param {Object} [opt]
* @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`
*/
export function setElementHoverStyle(el, hoverStl) {
hoverStl = el.__hoverStl = hoverStl !== false && (hoverStl || {});
el.__hoverStlDirty = true;
if (el.__highlighted) {
doSingleLeaveHover(el);
doSingleEnterHover(el);
}
}
/**
* Emphasis (called by API) has higher priority than `mouseover`.
* When element has been called to be entered emphasis, mouse over
* should not trigger the highlight effect (for example, animation
* scale) again, and `mouseout` should not downplay the highlight
* effect. So the listener of `mouseover` and `mouseout` should
* check `isInEmphasis`.
*
* @param {module:zrender/Element} el
* @return {boolean}
*/
export function isInEmphasis(el) {
return el && el.__isEmphasisEntered;
}
function onElementMouseOver(e) {
if (this.__hoverSilentOnTouch && e.zrByTouch) {
return;
} // Only if element is not in emphasis status
!this.__isEmphasisEntered && traverseCall(this, doSingleEnterHover);
}
function onElementMouseOut(e) {
if (this.__hoverSilentOnTouch && e.zrByTouch) {
return;
} // Only if element is not in emphasis status
!this.__isEmphasisEntered && traverseCall(this, doSingleLeaveHover);
}
function enterEmphasis() {
this.__isEmphasisEntered = true;
traverseCall(this, doSingleEnterHover);
}
function leaveEmphasis() {
this.__isEmphasisEntered = false;
traverseCall(this, doSingleLeaveHover);
}
/**
* Set hover style of element.
*
* [Caveat]:
* This method can be called repeatly and achieve the same result.
*
* [Usage]:
* Call the method for a "root" element once. Do not call it for each descendants.
* If the descendants elemenets of a group has itself hover style different from the
* root group, we can simply mount the style on `el.hoverStyle` for them, but should
* not call this method for them.
*
* @param {module:zrender/Element} el
* @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`.
* @param {Object} [opt]
* @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`.
*/
export function setHoverStyle(el, hoverStyle, opt) {
el.isGroup ? el.traverse(function (child) {
// If element has sepcified hoverStyle, then use it instead of given hoverStyle
// Often used when item group has a label element and it's hoverStyle is different
!child.isGroup && setElementHoverStyle(child, child.hoverStyle || hoverStyle);
}) : setElementHoverStyle(el, el.hoverStyle || hoverStyle);
setAsHoverStyleTrigger(el, opt);
}
/**
* @param {Object|boolean} [opt] If `false`, means disable trigger.
* @param {boolean} [opt.hoverSilentOnTouch=false]
* In touch device, mouseover event will be trigger on touchstart event
* (see module:zrender/dom/HandlerProxy). By this mechanism, we can
* conveniently use hoverStyle when tap on touch screen without additional
* code for compatibility.
* But if the chart/component has select feature, which usually also use
* hoverStyle, there might be conflict between 'select-highlight' and
* 'hover-highlight' especially when roam is enabled (see geo for example).
* In this case, hoverSilentOnTouch should be used to disable hover-highlight
* on touch device.
*/
export function setAsHoverStyleTrigger(el, opt) {
var disable = opt === false;
el.__hoverSilentOnTouch = opt != null && opt.hoverSilentOnTouch; // Simple optimize, since this method might be
// called for each elements of a group in some cases.
if (!disable || el.__hoverStyleTrigger) {
var method = disable ? 'off' : 'on'; // Duplicated function will be auto-ignored, see Eventful.js.
el[method]('mouseover', onElementMouseOver)[method]('mouseout', onElementMouseOut); // Emphasis, normal can be triggered manually
el[method]('emphasis', enterEmphasis)[method]('normal', leaveEmphasis);
el.__hoverStyleTrigger = !disable;
}
}
/**
* @param {Object|module:zrender/graphic/Style} normalStyle
* @param {Object} emphasisStyle
* @param {module:echarts/model/Model} normalModel
* @param {module:echarts/model/Model} emphasisModel
* @param {Object} opt Check `opt` of `setTextStyleCommon` to find other props.
* @param {string|Function} [opt.defaultText]
* @param {module:echarts/model/Model} [opt.labelFetcher] Fetch text by
* `opt.labelFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
* @param {module:echarts/model/Model} [opt.labelDataIndex] Fetch text by
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
* @param {module:echarts/model/Model} [opt.labelDimIndex] Fetch text by
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
* @param {Object} [normalSpecified]
* @param {Object} [emphasisSpecified]
*/
export function setLabelStyle(normalStyle, emphasisStyle, normalModel, emphasisModel, opt, normalSpecified, emphasisSpecified) {
opt = opt || EMPTY_OBJ;
var labelFetcher = opt.labelFetcher;
var labelDataIndex = opt.labelDataIndex;
var labelDimIndex = opt.labelDimIndex; // This scenario, `label.normal.show = true; label.emphasis.show = false`,
// is not supported util someone requests.
var showNormal = normalModel.getShallow('show');
var showEmphasis = emphasisModel.getShallow('show'); // Consider performance, only fetch label when necessary.
// If `normal.show` is `false` and `emphasis.show` is `true` and `emphasis.formatter` is not set,
// label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`.
var baseText;
if (showNormal || showEmphasis) {
if (labelFetcher) {
baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex);
}
if (baseText == null) {
baseText = zrUtil.isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText;
}
}
var normalStyleText = showNormal ? baseText : null;
var emphasisStyleText = showEmphasis ? zrUtil.retrieve2(labelFetcher ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex) : null, baseText) : null; // Optimize: If style.text is null, text will not be drawn.
if (normalStyleText != null || emphasisStyleText != null) {
// Always set `textStyle` even if `normalStyle.text` is null, because default
// values have to be set on `normalStyle`.
// If we set default values on `emphasisStyle`, consider case:
// Firstly, `setOption(... label: {normal: {text: null}, emphasis: {show: true}} ...);`
// Secondly, `setOption(... label: {noraml: {show: true, text: 'abc', color: 'red'} ...);`
// Then the 'red' will not work on emphasis.
setTextStyle(normalStyle, normalModel, normalSpecified, opt);
setTextStyle(emphasisStyle, emphasisModel, emphasisSpecified, opt, true);
}
normalStyle.text = normalStyleText;
emphasisStyle.text = emphasisStyleText;
}
/**
* Set basic textStyle properties.
* @param {Object|module:zrender/graphic/Style} textStyle
* @param {module:echarts/model/Model} model
* @param {Object} [specifiedTextStyle] Can be overrided by settings in model.
* @param {Object} [opt] See `opt` of `setTextStyleCommon`.
* @param {boolean} [isEmphasis]
*/
export function setTextStyle(textStyle, textStyleModel, specifiedTextStyle, opt, isEmphasis) {
setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis);
specifiedTextStyle && zrUtil.extend(textStyle, specifiedTextStyle); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
return textStyle;
}
/**
* Set text option in the style.
* @deprecated
* @param {Object} textStyle
* @param {module:echarts/model/Model} labelModel
* @param {string|boolean} defaultColor Default text color.
* If set as false, it will be processed as a emphasis style.
*/
export function setText(textStyle, labelModel, defaultColor) {
var opt = {
isRectText: true
};
var isEmphasis;
if (defaultColor === false) {
isEmphasis = true;
} else {
// Support setting color as 'auto' to get visual color.
opt.autoColor = defaultColor;
}
setTextStyleCommon(textStyle, labelModel, opt, isEmphasis); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
}
/**
* {
* disableBox: boolean, Whether diable drawing box of block (outer most).
* isRectText: boolean,
* autoColor: string, specify a color when color is 'auto',
* for textFill, textStroke, textBackgroundColor, and textBorderColor.
* If autoColor specified, it is used as default textFill.
* useInsideStyle:
* `true`: Use inside style (textFill, textStroke, textStrokeWidth)
* if `textFill` is not specified.
* `false`: Do not use inside style.
* `null/undefined`: use inside style if `isRectText` is true and
* `textFill` is not specified and textPosition contains `'inside'`.
* forceRich: boolean
* }
*/
function setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis) {
// Consider there will be abnormal when merge hover style to normal style if given default value.
opt = opt || EMPTY_OBJ;
if (opt.isRectText) {
var textPosition = textStyleModel.getShallow('position') || (isEmphasis ? null : 'inside'); // 'outside' is not a valid zr textPostion value, but used
// in bar series, and magric type should be considered.
textPosition === 'outside' && (textPosition = 'top');
textStyle.textPosition = textPosition;
textStyle.textOffset = textStyleModel.getShallow('offset');
var labelRotate = textStyleModel.getShallow('rotate');
labelRotate != null && (labelRotate *= Math.PI / 180);
textStyle.textRotation = labelRotate;
textStyle.textDistance = zrUtil.retrieve2(textStyleModel.getShallow('distance'), isEmphasis ? null : 5);
}
var ecModel = textStyleModel.ecModel;
var globalTextStyle = ecModel && ecModel.option.textStyle; // Consider case:
// {
// data: [{
// value: 12,
// label: {
// rich: {
// // no 'a' here but using parent 'a'.
// }
// }
// }],
// rich: {
// a: { ... }
// }
// }
var richItemNames = getRichItemNames(textStyleModel);
var richResult;
if (richItemNames) {
richResult = {};
for (var name in richItemNames) {
if (richItemNames.hasOwnProperty(name)) {
// Cascade is supported in rich.
var richTextStyle = textStyleModel.getModel(['rich', name]); // In rich, never `disableBox`.
setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isEmphasis);
}
}
}
textStyle.rich = richResult;
setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, true);
if (opt.forceRich && !opt.textStyle) {
opt.textStyle = {};
}
return textStyle;
} // Consider case:
// {
// data: [{
// value: 12,
// label: {
// rich: {
// // no 'a' here but using parent 'a'.
// }
// }
// }],
// rich: {
// a: { ... }
// }
// }
function getRichItemNames(textStyleModel) {
// Use object to remove duplicated names.
var richItemNameMap;
while (textStyleModel && textStyleModel !== textStyleModel.ecModel) {
var rich = (textStyleModel.option || EMPTY_OBJ).rich;
if (rich) {
richItemNameMap = richItemNameMap || {};
for (var name in rich) {
if (rich.hasOwnProperty(name)) {
richItemNameMap[name] = 1;
}
}
}
textStyleModel = textStyleModel.parentModel;
}
return richItemNameMap;
}
function setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isBlock) {
// In merge mode, default value should not be given.
globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ;
textStyle.textFill = getAutoColor(textStyleModel.getShallow('color'), opt) || globalTextStyle.color;
textStyle.textStroke = getAutoColor(textStyleModel.getShallow('textBorderColor'), opt) || globalTextStyle.textBorderColor;
textStyle.textStrokeWidth = zrUtil.retrieve2(textStyleModel.getShallow('textBorderWidth'), globalTextStyle.textBorderWidth); // Save original textPosition, because style.textPosition will be repalced by
// real location (like [10, 30]) in zrender.
textStyle.insideRawTextPosition = textStyle.textPosition;
if (!isEmphasis) {
if (isBlock) {
textStyle.insideRollbackOpt = opt;
applyDefaultTextStyle(textStyle);
} // Set default finally.
if (textStyle.textFill == null) {
textStyle.textFill = opt.autoColor;
}
} // Do not use `getFont` here, because merge should be supported, where
// part of these properties may be changed in emphasis style, and the
// others should remain their original value got from normal style.
textStyle.fontStyle = textStyleModel.getShallow('fontStyle') || globalTextStyle.fontStyle;
textStyle.fontWeight = textStyleModel.getShallow('fontWeight') || globalTextStyle.fontWeight;
textStyle.fontSize = textStyleModel.getShallow('fontSize') || globalTextStyle.fontSize;
textStyle.fontFamily = textStyleModel.getShallow('fontFamily') || globalTextStyle.fontFamily;
textStyle.textAlign = textStyleModel.getShallow('align');
textStyle.textVerticalAlign = textStyleModel.getShallow('verticalAlign') || textStyleModel.getShallow('baseline');
textStyle.textLineHeight = textStyleModel.getShallow('lineHeight');
textStyle.textWidth = textStyleModel.getShallow('width');
textStyle.textHeight = textStyleModel.getShallow('height');
textStyle.textTag = textStyleModel.getShallow('tag');
if (!isBlock || !opt.disableBox) {
textStyle.textBackgroundColor = getAutoColor(textStyleModel.getShallow('backgroundColor'), opt);
textStyle.textPadding = textStyleModel.getShallow('padding');
textStyle.textBorderColor = getAutoColor(textStyleModel.getShallow('borderColor'), opt);
textStyle.textBorderWidth = textStyleModel.getShallow('borderWidth');
textStyle.textBorderRadius = textStyleModel.getShallow('borderRadius');
textStyle.textBoxShadowColor = textStyleModel.getShallow('shadowColor');
textStyle.textBoxShadowBlur = textStyleModel.getShallow('shadowBlur');
textStyle.textBoxShadowOffsetX = textStyleModel.getShallow('shadowOffsetX');
textStyle.textBoxShadowOffsetY = textStyleModel.getShallow('shadowOffsetY');
}
textStyle.textShadowColor = textStyleModel.getShallow('textShadowColor') || globalTextStyle.textShadowColor;
textStyle.textShadowBlur = textStyleModel.getShallow('textShadowBlur') || globalTextStyle.textShadowBlur;
textStyle.textShadowOffsetX = textStyleModel.getShallow('textShadowOffsetX') || globalTextStyle.textShadowOffsetX;
textStyle.textShadowOffsetY = textStyleModel.getShallow('textShadowOffsetY') || globalTextStyle.textShadowOffsetY;
}
function getAutoColor(color, opt) {
return color !== 'auto' ? color : opt && opt.autoColor ? opt.autoColor : null;
} // When text position is `inside` and `textFill` not specified, we
// provide a mechanism to auto make text border for better view. But
// text position changing when hovering or being emphasis should be
// considered, where the `insideRollback` enables to restore the style.
function applyDefaultTextStyle(textStyle) {
var opt = textStyle.insideRollbackOpt; // Only insideRollbackOpt create (setTextStyleCommon used),
// applyDefaultTextStyle works.
if (!opt || textStyle.textFill != null) {
return;
}
var useInsideStyle = opt.useInsideStyle;
var textPosition = textStyle.insideRawTextPosition;
var insideRollback;
var autoColor = opt.autoColor;
if (useInsideStyle !== false && (useInsideStyle === true || opt.isRectText && textPosition // textPosition can be [10, 30]
&& typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0)) {
insideRollback = {
textFill: null,
textStroke: textStyle.textStroke,
textStrokeWidth: textStyle.textStrokeWidth
};
textStyle.textFill = '#fff'; // Consider text with #fff overflow its container.
if (textStyle.textStroke == null) {
textStyle.textStroke = autoColor;
textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2);
}
} else if (autoColor != null) {
insideRollback = {
textFill: null
};
textStyle.textFill = autoColor;
} // Always set `insideRollback`, for clearing previous.
if (insideRollback) {
textStyle.insideRollback = insideRollback;
}
}
function rollbackDefaultTextStyle(style) {
var insideRollback = style.insideRollback;
if (insideRollback) {
style.textFill = insideRollback.textFill;
style.textStroke = insideRollback.textStroke;
style.textStrokeWidth = insideRollback.textStrokeWidth;
style.insideRollback = null;
}
}
export function getFont(opt, ecModel) {
// ecModel or default text style model.
var gTextStyleModel = ecModel || ecModel.getModel('textStyle');
return zrUtil.trim([// FIXME in node-canvas fontWeight is before fontStyle
opt.fontStyle || gTextStyleModel && gTextStyleModel.getShallow('fontStyle') || '', opt.fontWeight || gTextStyleModel && gTextStyleModel.getShallow('fontWeight') || '', (opt.fontSize || gTextStyleModel && gTextStyleModel.getShallow('fontSize') || 12) + 'px', opt.fontFamily || gTextStyleModel && gTextStyleModel.getShallow('fontFamily') || 'sans-serif'].join(' '));
}
function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) {
if (typeof dataIndex === 'function') {
cb = dataIndex;
dataIndex = null;
} // Do not check 'animation' property directly here. Consider this case:
// animation model is an `itemModel`, whose does not have `isAnimationEnabled`
// but its parent model (`seriesModel`) does.
var animationEnabled = animatableModel && animatableModel.isAnimationEnabled();
if (animationEnabled) {
var postfix = isUpdate ? 'Update' : '';
var duration = animatableModel.getShallow('animationDuration' + postfix);
var animationEasing = animatableModel.getShallow('animationEasing' + postfix);
var animationDelay = animatableModel.getShallow('animationDelay' + postfix);
if (typeof animationDelay === 'function') {
animationDelay = animationDelay(dataIndex, animatableModel.getAnimationDelayParams ? animatableModel.getAnimationDelayParams(el, dataIndex) : null);
}
if (typeof duration === 'function') {
duration = duration(dataIndex);
}
duration > 0 ? el.animateTo(props, duration, animationDelay || 0, animationEasing, cb, !!cb) : (el.stopAnimation(), el.attr(props), cb && cb());
} else {
el.stopAnimation();
el.attr(props);
cb && cb();
}
}
/**
* Update graphic element properties with or without animation according to the
* configuration in series.
*
* Caution: this method will stop previous animation.
* So if do not use this method to one element twice before
* animation starts, unless you know what you are doing.
*
* @param {module:zrender/Element} el
* @param {Object} props
* @param {module:echarts/model/Model} [animatableModel]
* @param {number} [dataIndex]
* @param {Function} [cb]
* @example
* graphic.updateProps(el, {
* position: [100, 100]
* }, seriesModel, dataIndex, function () { console.log('Animation done!'); });
* // Or
* graphic.updateProps(el, {
* position: [100, 100]
* }, seriesModel, function () { console.log('Animation done!'); });
*/
export function updateProps(el, props, animatableModel, dataIndex, cb) {
animateOrSetProps(true, el, props, animatableModel, dataIndex, cb);
}
/**
* Init graphic element properties with or without animation according to the
* configuration in series.
*
* Caution: this method will stop previous animation.
* So if do not use this method to one element twice before
* animation starts, unless you know what you are doing.
*
* @param {module:zrender/Element} el
* @param {Object} props
* @param {module:echarts/model/Model} [animatableModel]
* @param {number} [dataIndex]
* @param {Function} cb
*/
export function initProps(el, props, animatableModel, dataIndex, cb) {
animateOrSetProps(false, el, props, animatableModel, dataIndex, cb);
}
/**
* Get transform matrix of target (param target),
* in coordinate of its ancestor (param ancestor)
*
* @param {module:zrender/mixin/Transformable} target
* @param {module:zrender/mixin/Transformable} [ancestor]
*/
export function getTransform(target, ancestor) {
var mat = matrix.identity([]);
while (target && target !== ancestor) {
matrix.mul(mat, target.getLocalTransform(), mat);
target = target.parent;
}
return mat;
}
/**
* Apply transform to an vertex.
* @param {Array.<number>} target [x, y]
* @param {Array.<number>|TypedArray.<number>|Object} transform Can be:
* + Transform matrix: like [1, 0, 0, 1, 0, 0]
* + {position, rotation, scale}, the same as `zrender/Transformable`.
* @param {boolean=} invert Whether use invert matrix.
* @return {Array.<number>} [x, y]
*/
export function applyTransform(target, transform, invert) {
if (transform && !zrUtil.isArrayLike(transform)) {
transform = Transformable.getLocalTransform(transform);
}
if (invert) {
transform = matrix.invert([], transform);
}
return vector.applyTransform([], target, transform);
}
/**
* @param {string} direction 'left' 'right' 'top' 'bottom'
* @param {Array.<number>} transform Transform matrix: like [1, 0, 0, 1, 0, 0]
* @param {boolean=} invert Whether use invert matrix.
* @return {string} Transformed direction. 'left' 'right' 'top' 'bottom'
*/
export function transformDirection(direction, transform, invert) {
// Pick a base, ensure that transform result will not be (0, 0).
var hBase = transform[4] === 0 || transform[5] === 0 || transform[0] === 0 ? 1 : Math.abs(2 * transform[4] / transform[0]);
var vBase = transform[4] === 0 || transform[5] === 0 || transform[2] === 0 ? 1 : Math.abs(2 * transform[4] / transform[2]);
var vertex = [direction === 'left' ? -hBase : direction === 'right' ? hBase : 0, direction === 'top' ? -vBase : direction === 'bottom' ? vBase : 0];
vertex = applyTransform(vertex, transform, invert);
return Math.abs(vertex[0]) > Math.abs(vertex[1]) ? vertex[0] > 0 ? 'right' : 'left' : vertex[1] > 0 ? 'bottom' : 'top';
}
/**
* Apply group transition animation from g1 to g2.
* If no animatableModel, no animation.
*/
export function groupTransition(g1, g2, animatableModel, cb) {
if (!g1 || !g2) {
return;
}
function getElMap(g) {
var elMap = {};
g.traverse(function (el) {
if (!el.isGroup && el.anid) {
elMap[el.anid] = el;
}
});
return elMap;
}
function getAnimatableProps(el) {
var obj = {
position: vector.clone(el.position),
rotation: el.rotation
};
if (el.shape) {
obj.shape = zrUtil.extend({}, el.shape);
}
return obj;
}
var elMap1 = getElMap(g1);
g2.traverse(function (el) {
if (!el.isGroup && el.anid) {
var oldEl = elMap1[el.anid];
if (oldEl) {
var newProp = getAnimatableProps(el);
el.attr(getAnimatableProps(oldEl));
updateProps(el, newProp, animatableModel, el.dataIndex);
} // else {
// if (el.previousProps) {
// graphic.updateProps
// }
// }
}
});
}
/**
* @param {Array.<Array.<number>>} points Like: [[23, 44], [53, 66], ...]
* @param {Object} rect {x, y, width, height}
* @return {Array.<Array.<number>>} A new clipped points.
*/
export function clipPointsByRect(points, rect) {
// FIXME: this way migth be incorrect when grpahic clipped by a corner.
// and when element have border.
return zrUtil.map(points, function (point) {
var x = point[0];
x = mathMax(x, rect.x);
x = mathMin(x, rect.x + rect.width);
var y = point[1];
y = mathMax(y, rect.y);
y = mathMin(y, rect.y + rect.height);
return [x, y];
});
}
/**
* @param {Object} targetRect {x, y, width, height}
* @param {Object} rect {x, y, width, height}
* @return {Object} A new clipped rect. If rect size are negative, return undefined.
*/
export function clipRectByRect(targetRect, rect) {
var x = mathMax(targetRect.x, rect.x);
var x2 = mathMin(targetRect.x + targetRect.width, rect.x + rect.width);
var y = mathMax(targetRect.y, rect.y);
var y2 = mathMin(targetRect.y + targetRect.height, rect.y + rect.height); // If the total rect is cliped, nothing, including the border,
// should be painted. So return undefined.
if (x2 >= x && y2 >= y) {
return {
x: x,
y: y,
width: x2 - x,
height: y2 - y
};
}
}
/**
* @param {string} iconStr Support 'image://' or 'path://' or direct svg path.
* @param {Object} [opt] Properties of `module:zrender/Element`, except `style`.
* @param {Object} [rect] {x, y, width, height}
* @return {module:zrender/Element} Icon path or image element.
*/
export function createIcon(iconStr, opt, rect) {
opt = zrUtil.extend({
rectHover: true
}, opt);
var style = opt.style = {
strokeNoScale: true
};
rect = rect || {
x: -1,
y: -1,
width: 2,
height: 2
};
if (iconStr) {
return iconStr.indexOf('image://') === 0 ? (style.image = iconStr.slice(8), zrUtil.defaults(style, rect), new ZImage(opt)) : makePath(iconStr.replace('path://', ''), opt, rect, 'center');
}
}
export { Group, ZImage as Image, Text, Circle, Sector, Ring, Polygon, Polyline, Rect, Line, BezierCurve, Arc, IncrementalDisplayable, CompoundPath, LinearGradient, RadialGradient, BoundingRect };