1527 lines
51 KiB
JavaScript
1527 lines
51 KiB
JavaScript
|
|
/*
|
|
* 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 zrUtil = require("zrender/lib/core/util");
|
|
|
|
var pathTool = require("zrender/lib/tool/path");
|
|
|
|
var colorTool = require("zrender/lib/tool/color");
|
|
|
|
var matrix = require("zrender/lib/core/matrix");
|
|
|
|
var vector = require("zrender/lib/core/vector");
|
|
|
|
var Path = require("zrender/lib/graphic/Path");
|
|
|
|
var Transformable = require("zrender/lib/mixin/Transformable");
|
|
|
|
var ZImage = require("zrender/lib/graphic/Image");
|
|
|
|
exports.Image = ZImage;
|
|
|
|
var Group = require("zrender/lib/container/Group");
|
|
|
|
exports.Group = Group;
|
|
|
|
var Text = require("zrender/lib/graphic/Text");
|
|
|
|
exports.Text = Text;
|
|
|
|
var Circle = require("zrender/lib/graphic/shape/Circle");
|
|
|
|
exports.Circle = Circle;
|
|
|
|
var Sector = require("zrender/lib/graphic/shape/Sector");
|
|
|
|
exports.Sector = Sector;
|
|
|
|
var Ring = require("zrender/lib/graphic/shape/Ring");
|
|
|
|
exports.Ring = Ring;
|
|
|
|
var Polygon = require("zrender/lib/graphic/shape/Polygon");
|
|
|
|
exports.Polygon = Polygon;
|
|
|
|
var Polyline = require("zrender/lib/graphic/shape/Polyline");
|
|
|
|
exports.Polyline = Polyline;
|
|
|
|
var Rect = require("zrender/lib/graphic/shape/Rect");
|
|
|
|
exports.Rect = Rect;
|
|
|
|
var Line = require("zrender/lib/graphic/shape/Line");
|
|
|
|
exports.Line = Line;
|
|
|
|
var BezierCurve = require("zrender/lib/graphic/shape/BezierCurve");
|
|
|
|
exports.BezierCurve = BezierCurve;
|
|
|
|
var Arc = require("zrender/lib/graphic/shape/Arc");
|
|
|
|
exports.Arc = Arc;
|
|
|
|
var CompoundPath = require("zrender/lib/graphic/CompoundPath");
|
|
|
|
exports.CompoundPath = CompoundPath;
|
|
|
|
var LinearGradient = require("zrender/lib/graphic/LinearGradient");
|
|
|
|
exports.LinearGradient = LinearGradient;
|
|
|
|
var RadialGradient = require("zrender/lib/graphic/RadialGradient");
|
|
|
|
exports.RadialGradient = RadialGradient;
|
|
|
|
var BoundingRect = require("zrender/lib/core/BoundingRect");
|
|
|
|
exports.BoundingRect = BoundingRect;
|
|
|
|
var IncrementalDisplayable = require("zrender/lib/graphic/IncrementalDisplayable");
|
|
|
|
exports.IncrementalDisplayable = IncrementalDisplayable;
|
|
|
|
var subPixelOptimizeUtil = require("zrender/lib/graphic/helper/subPixelOptimize");
|
|
|
|
/*
|
|
* 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 mathMax = Math.max;
|
|
var mathMin = Math.min;
|
|
var EMPTY_OBJ = {};
|
|
var Z2_EMPHASIS_LIFT = 1; // key: label model property nane, value: style property name.
|
|
|
|
var CACHED_LABEL_STYLE_PROPERTIES = {
|
|
color: 'textFill',
|
|
textBorderColor: 'textStroke',
|
|
textBorderWidth: 'textStrokeWidth'
|
|
};
|
|
var EMPHASIS = 'emphasis';
|
|
var NORMAL = 'normal'; // Reserve 0 as default.
|
|
|
|
var _highlightNextDigit = 1;
|
|
var _highlightKeyMap = {};
|
|
var _customShapeMap = {};
|
|
/**
|
|
* Extend shape with parameters
|
|
*/
|
|
|
|
function extendShape(opts) {
|
|
return Path.extend(opts);
|
|
}
|
|
/**
|
|
* Extend path
|
|
*/
|
|
|
|
|
|
function extendPath(pathData, opts) {
|
|
return pathTool.extendFromString(pathData, opts);
|
|
}
|
|
/**
|
|
* Register a user defined shape.
|
|
* The shape class can be fetched by `getShapeClass`
|
|
* This method will overwrite the registered shapes, including
|
|
* the registered built-in shapes, if using the same `name`.
|
|
* The shape can be used in `custom series` and
|
|
* `graphic component` by declaring `{type: name}`.
|
|
*
|
|
* @param {string} name
|
|
* @param {Object} ShapeClass Can be generated by `extendShape`.
|
|
*/
|
|
|
|
|
|
function registerShape(name, ShapeClass) {
|
|
_customShapeMap[name] = ShapeClass;
|
|
}
|
|
/**
|
|
* Find shape class registered by `registerShape`. Usually used in
|
|
* fetching user defined shape.
|
|
*
|
|
* [Caution]:
|
|
* (1) This method **MUST NOT be used inside echarts !!!**, unless it is prepared
|
|
* to use user registered shapes.
|
|
* Because the built-in shape (see `getBuiltInShape`) will be registered by
|
|
* `registerShape` by default. That enables users to get both built-in
|
|
* shapes as well as the shapes belonging to themsleves. But users can overwrite
|
|
* the built-in shapes by using names like 'circle', 'rect' via calling
|
|
* `registerShape`. So the echarts inner featrues should not fetch shapes from here
|
|
* in case that it is overwritten by users, except that some features, like
|
|
* `custom series`, `graphic component`, do it deliberately.
|
|
*
|
|
* (2) In the features like `custom series`, `graphic component`, the user input
|
|
* `{tpye: 'xxx'}` does not only specify shapes but also specify other graphic
|
|
* elements like `'group'`, `'text'`, `'image'` or event `'path'`. Those names
|
|
* are reserved names, that is, if some user register a shape named `'image'`,
|
|
* the shape will not be used. If we intending to add some more reserved names
|
|
* in feature, that might bring break changes (disable some existing user shape
|
|
* names). But that case probably rearly happen. So we dont make more mechanism
|
|
* to resolve this issue here.
|
|
*
|
|
* @param {string} name
|
|
* @return {Object} The shape class. If not found, return nothing.
|
|
*/
|
|
|
|
|
|
function getShapeClass(name) {
|
|
if (_customShapeMap.hasOwnProperty(name)) {
|
|
return _customShapeMap[name];
|
|
}
|
|
}
|
|
/**
|
|
* 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'
|
|
*/
|
|
|
|
|
|
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'
|
|
*/
|
|
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
var mergePath = pathTool.mergePath;
|
|
/**
|
|
* Resize a path to fit the rect
|
|
* @param {module:zrender/graphic/Path} path
|
|
* @param {Object} rect
|
|
*/
|
|
|
|
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
|
|
*/
|
|
|
|
|
|
function subPixelOptimizeLine(param) {
|
|
subPixelOptimizeUtil.subPixelOptimizeLine(param.shape, param.shape, param.style);
|
|
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
|
|
*/
|
|
|
|
|
|
function subPixelOptimizeRect(param) {
|
|
subPixelOptimizeUtil.subPixelOptimizeRect(param.shape, param.shape, param.style);
|
|
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.
|
|
*/
|
|
|
|
|
|
var subPixelOptimize = subPixelOptimizeUtil.subPixelOptimize;
|
|
|
|
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.__cachedNormalStl = el.__cachedNormalZ2 = null;
|
|
return;
|
|
}
|
|
|
|
var normalStyle = el.__cachedNormalStl = {};
|
|
el.__cachedNormalZ2 = el.z2;
|
|
var elStyle = el.style;
|
|
|
|
for (var name in hoverStyle) {
|
|
// See comment in `singleEnterEmphasis`.
|
|
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 singleEnterEmphasis(el) {
|
|
var hoverStl = el.__hoverStl;
|
|
|
|
if (!hoverStl || el.__highlighted) {
|
|
return;
|
|
}
|
|
|
|
var zr = el.__zr;
|
|
var useHoverLayer = el.useHoverLayer && zr && zr.painter.type === 'canvas';
|
|
el.__highlighted = useHoverLayer ? 'layer' : 'plain';
|
|
|
|
if (el.isGroup || !zr && el.useHoverLayer) {
|
|
return;
|
|
}
|
|
|
|
var elTarget = el;
|
|
var targetStyle = el.style;
|
|
|
|
if (useHoverLayer) {
|
|
elTarget = zr.addHover(el);
|
|
targetStyle = elTarget.style;
|
|
}
|
|
|
|
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.
|
|
// But we choose the merge strategy that only properties that is not `null/undefined`.
|
|
// Because when making a textStyle (espacially rich text), it is not easy to distinguish
|
|
// `hasOwnProperty` and `null/undefined` in code, so we trade them as the same for simplicity.
|
|
// But this strategy brings a trouble that `null/undefined` can not be used to remove
|
|
// style any more in `emphasis`. Users can both set properties directly on normal and
|
|
// emphasis to avoid this issue, or we might support `'none'` for this case if required.
|
|
|
|
|
|
targetStyle.extendFrom(hoverStl);
|
|
setDefaultHoverFillStroke(targetStyle, hoverStl, 'fill');
|
|
setDefaultHoverFillStroke(targetStyle, hoverStl, 'stroke');
|
|
applyDefaultTextStyle(targetStyle);
|
|
|
|
if (!useHoverLayer) {
|
|
el.dirty(false);
|
|
el.z2 += Z2_EMPHASIS_LIFT;
|
|
}
|
|
}
|
|
|
|
function setDefaultHoverFillStroke(targetStyle, hoverStyle, prop) {
|
|
if (!hasFillOrStroke(hoverStyle[prop]) && hasFillOrStroke(targetStyle[prop])) {
|
|
targetStyle[prop] = liftColor(targetStyle[prop]);
|
|
}
|
|
}
|
|
|
|
function singleEnterNormal(el) {
|
|
var highlighted = el.__highlighted;
|
|
|
|
if (!highlighted) {
|
|
return;
|
|
}
|
|
|
|
el.__highlighted = false;
|
|
|
|
if (el.isGroup) {
|
|
return;
|
|
}
|
|
|
|
if (highlighted === 'layer') {
|
|
el.__zr && el.__zr.removeHover(el);
|
|
} else {
|
|
var style = el.style;
|
|
var normalStl = el.__cachedNormalStl;
|
|
|
|
if (normalStl) {
|
|
rollbackDefaultTextStyle(style);
|
|
el.setStyle(normalStl);
|
|
applyDefaultTextStyle(style);
|
|
} // `__cachedNormalZ2` will not be reset if calling `setElementHoverStyle`
|
|
// when `el` is on emphasis state. So here by comparing with 1, we try
|
|
// hard to make the bug case rare.
|
|
|
|
|
|
var normalZ2 = el.__cachedNormalZ2;
|
|
|
|
if (normalZ2 != null && el.z2 - normalZ2 === Z2_EMPHASIS_LIFT) {
|
|
el.z2 = normalZ2;
|
|
}
|
|
}
|
|
}
|
|
|
|
function traverseUpdate(el, updater, commonParam) {
|
|
// If root is group, also enter updater for `highDownOnUpdate`.
|
|
var fromState = NORMAL;
|
|
var toState = NORMAL;
|
|
var trigger; // See the rule of `highDownOnUpdate` on `graphic.setAsHighDownDispatcher`.
|
|
|
|
el.__highlighted && (fromState = EMPHASIS, trigger = true);
|
|
updater(el, commonParam);
|
|
el.__highlighted && (toState = EMPHASIS, trigger = true);
|
|
el.isGroup && el.traverse(function (child) {
|
|
!child.isGroup && updater(child, commonParam);
|
|
});
|
|
trigger && el.__highDownOnUpdate && el.__highDownOnUpdate(fromState, toState);
|
|
}
|
|
/**
|
|
* Set hover style (namely "emphasis style") of element, based on the current
|
|
* style of the given `el`.
|
|
* This method should be called after all of the normal styles have been adopted
|
|
* to the `el`. See the reason on `setHoverStyle`.
|
|
*
|
|
* @param {module:zrender/Element} el Should not be `zrender/container/Group`.
|
|
* @param {Object} [el.hoverStyle] Can be set on el or its descendants,
|
|
* e.g., `el.hoverStyle = ...; graphic.setHoverStyle(el); `.
|
|
* Often used when item group has a label element and it's hoverStyle is different.
|
|
* @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.
|
|
*/
|
|
|
|
|
|
function setElementHoverStyle(el, hoverStl) {
|
|
// For performance consideration, it might be better to make the "hover style" only the
|
|
// difference properties from the "normal style", but not a entire copy of all styles.
|
|
hoverStl = el.__hoverStl = hoverStl !== false && (el.hoverStyle || hoverStl || {});
|
|
el.__hoverStlDirty = true; // FIXME
|
|
// It is not completely right to save "normal"/"emphasis" flag on elements.
|
|
// It probably should be saved on `data` of series. Consider the cases:
|
|
// (1) A highlighted elements are moved out of the view port and re-enter
|
|
// again by dataZoom.
|
|
// (2) call `setOption` and replace elements totally when they are highlighted.
|
|
|
|
if (el.__highlighted) {
|
|
// Consider the case:
|
|
// The styles of a highlighted `el` is being updated. The new "emphasis style"
|
|
// should be adapted to the `el`. Notice here new "normal styles" should have
|
|
// been set outside and the cached "normal style" is out of date.
|
|
el.__cachedNormalStl = null; // Do not clear `__cachedNormalZ2` here, because setting `z2` is not a constraint
|
|
// of this method. In most cases, `z2` is not set and hover style should be able
|
|
// to rollback. Of course, that would bring bug, but only in a rare case, see
|
|
// `doSingleLeaveHover` for details.
|
|
|
|
singleEnterNormal(el);
|
|
singleEnterEmphasis(el);
|
|
}
|
|
}
|
|
|
|
function onElementMouseOver(e) {
|
|
!shouldSilent(this, e) // "emphasis" event highlight has higher priority than mouse highlight.
|
|
&& !this.__highByOuter && traverseUpdate(this, singleEnterEmphasis);
|
|
}
|
|
|
|
function onElementMouseOut(e) {
|
|
!shouldSilent(this, e) // "emphasis" event highlight has higher priority than mouse highlight.
|
|
&& !this.__highByOuter && traverseUpdate(this, singleEnterNormal);
|
|
}
|
|
|
|
function onElementEmphasisEvent(highlightDigit) {
|
|
this.__highByOuter |= 1 << (highlightDigit || 0);
|
|
traverseUpdate(this, singleEnterEmphasis);
|
|
}
|
|
|
|
function onElementNormalEvent(highlightDigit) {
|
|
!(this.__highByOuter &= ~(1 << (highlightDigit || 0))) && traverseUpdate(this, singleEnterNormal);
|
|
}
|
|
|
|
function shouldSilent(el, e) {
|
|
return el.__highDownSilentOnTouch && e.zrByTouch;
|
|
}
|
|
/**
|
|
* Set hover style (namely "emphasis style") of element,
|
|
* based on the current style of the given `el`.
|
|
*
|
|
* (1)
|
|
* **CONSTRAINTS** for this method:
|
|
* <A> This method MUST be called after all of the normal styles having been adopted
|
|
* to the `el`.
|
|
* <B> The input `hoverStyle` (that is, "emphasis style") MUST be the subset of the
|
|
* "normal style" having been set to the el.
|
|
* <C> `color` MUST be one of the "normal styles" (because color might be lifted as
|
|
* a default hover style).
|
|
*
|
|
* The reason: this method treat the current style of the `el` as the "normal style"
|
|
* and cache them when enter/update the "emphasis style". Consider the case: the `el`
|
|
* is in "emphasis" state and `setOption`/`dispatchAction` trigger the style updating
|
|
* logic, where the el should shift from the original emphasis style to the new
|
|
* "emphasis style" and should be able to "downplay" back to the new "normal style".
|
|
*
|
|
* Indeed, it is error-prone to make a interface has so many constraints, but I have
|
|
* not found a better solution yet to fit the backward compatibility, performance and
|
|
* the current programming style.
|
|
*
|
|
* (2)
|
|
* 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.
|
|
*
|
|
* (3) These input parameters can be set directly on `el`:
|
|
*
|
|
* @param {module:zrender/Element} el
|
|
* @param {Object} [el.hoverStyle] See `graphic.setElementHoverStyle`.
|
|
* @param {boolean} [el.highDownSilentOnTouch=false] See `graphic.setAsHighDownDispatcher`.
|
|
* @param {Function} [el.highDownOnUpdate] See `graphic.setAsHighDownDispatcher`.
|
|
* @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`.
|
|
*/
|
|
|
|
|
|
function setHoverStyle(el, hoverStyle) {
|
|
setAsHighDownDispatcher(el, true);
|
|
traverseUpdate(el, setElementHoverStyle, hoverStyle);
|
|
}
|
|
/**
|
|
* @param {module:zrender/Element} el
|
|
* @param {Function} [el.highDownOnUpdate] Called when state updated.
|
|
* Since `setHoverStyle` has the constraint that it must be called after
|
|
* all of the normal style updated, `highDownOnUpdate` is not needed to
|
|
* trigger if both `fromState` and `toState` is 'normal', and needed to
|
|
* trigger if both `fromState` and `toState` is 'emphasis', which enables
|
|
* to sync outside style settings to "emphasis" state.
|
|
* @this {string} This dispatcher `el`.
|
|
* @param {string} fromState Can be "normal" or "emphasis".
|
|
* `fromState` might equal to `toState`,
|
|
* for example, when this method is called when `el` is
|
|
* on "emphasis" state.
|
|
* @param {string} toState Can be "normal" or "emphasis".
|
|
*
|
|
* FIXME
|
|
* CAUTION: Do not expose `highDownOnUpdate` outside echarts.
|
|
* Because it is not a complete solution. The update
|
|
* listener should not have been mount in element,
|
|
* and the normal/emphasis state should not have
|
|
* mantained on elements.
|
|
*
|
|
* @param {boolean} [el.highDownSilentOnTouch=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, `highDownSilentOnTouch` should be used to disable
|
|
* hover-highlight on touch device.
|
|
* @param {boolean} [asDispatcher=true] If `false`, do not set as "highDownDispatcher".
|
|
*/
|
|
|
|
|
|
function setAsHighDownDispatcher(el, asDispatcher) {
|
|
var disable = asDispatcher === false; // Make `highDownSilentOnTouch` and `highDownOnUpdate` only work after
|
|
// `setAsHighDownDispatcher` called. Avoid it is modified by user unexpectedly.
|
|
|
|
el.__highDownSilentOnTouch = el.highDownSilentOnTouch;
|
|
el.__highDownOnUpdate = el.highDownOnUpdate; // Simple optimize, since this method might be
|
|
// called for each elements of a group in some cases.
|
|
|
|
if (!disable || el.__highDownDispatcher) {
|
|
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 by API or other components like hover link.
|
|
|
|
el[method]('emphasis', onElementEmphasisEvent)[method]('normal', onElementNormalEvent); // Also keep previous record.
|
|
|
|
el.__highByOuter = el.__highByOuter || 0;
|
|
el.__highDownDispatcher = !disable;
|
|
}
|
|
}
|
|
/**
|
|
* @param {module:zrender/src/Element} el
|
|
* @return {boolean}
|
|
*/
|
|
|
|
|
|
function isHighDownDispatcher(el) {
|
|
return !!(el && el.__highDownDispatcher);
|
|
}
|
|
/**
|
|
* Support hightlight/downplay record on each elements.
|
|
* For the case: hover highlight/downplay (legend, visualMap, ...) and
|
|
* user triggerred hightlight/downplay should not conflict.
|
|
* Only all of the highlightDigit cleared, return to normal.
|
|
* @param {string} highlightKey
|
|
* @return {number} highlightDigit
|
|
*/
|
|
|
|
|
|
function getHighlightDigit(highlightKey) {
|
|
var highlightDigit = _highlightKeyMap[highlightKey];
|
|
|
|
if (highlightDigit == null && _highlightNextDigit <= 32) {
|
|
highlightDigit = _highlightKeyMap[highlightKey] = _highlightNextDigit++;
|
|
}
|
|
|
|
return highlightDigit;
|
|
}
|
|
/**
|
|
* See more info in `setTextStyleCommon`.
|
|
* @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, opt.labelProp)`
|
|
* @param {number} [opt.labelDataIndex] Fetch text by
|
|
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)`
|
|
* @param {number} [opt.labelDimIndex] Fetch text by
|
|
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)`
|
|
* @param {string} [opt.labelProp] Fetch text by
|
|
* `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex, opt.labelProp)`
|
|
* @param {Object} [normalSpecified]
|
|
* @param {Object} [emphasisSpecified]
|
|
*/
|
|
|
|
|
|
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;
|
|
var labelProp = opt.labelProp; // 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, labelProp);
|
|
}
|
|
|
|
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, labelProp) : 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;
|
|
}
|
|
/**
|
|
* Modify label style manually.
|
|
* Only works after `setLabelStyle` and `setElementHoverStyle` called.
|
|
*
|
|
* @param {module:zrender/src/Element} el
|
|
* @param {Object} [normalStyleProps] optional
|
|
* @param {Object} [emphasisStyleProps] optional
|
|
*/
|
|
|
|
|
|
function modifyLabelStyle(el, normalStyleProps, emphasisStyleProps) {
|
|
var elStyle = el.style;
|
|
|
|
if (normalStyleProps) {
|
|
rollbackDefaultTextStyle(elStyle);
|
|
el.setStyle(normalStyleProps);
|
|
applyDefaultTextStyle(elStyle);
|
|
}
|
|
|
|
elStyle = el.__hoverStl;
|
|
|
|
if (emphasisStyleProps && elStyle) {
|
|
rollbackDefaultTextStyle(elStyle);
|
|
zrUtil.extend(elStyle, emphasisStyleProps);
|
|
applyDefaultTextStyle(elStyle);
|
|
}
|
|
}
|
|
/**
|
|
* Set basic textStyle properties.
|
|
* See more info in `setTextStyleCommon`.
|
|
* @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]
|
|
*/
|
|
|
|
|
|
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.
|
|
* See more info in `setTextStyleCommon`.
|
|
* @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.
|
|
*/
|
|
|
|
|
|
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);
|
|
}
|
|
/**
|
|
* The uniform entry of set text style, that is, retrieve style definitions
|
|
* from `model` and set to `textStyle` object.
|
|
*
|
|
* Never in merge mode, but in overwrite mode, that is, all of the text style
|
|
* properties will be set. (Consider the states of normal and emphasis and
|
|
* default value can be adopted, merge would make the logic too complicated
|
|
* to manage.)
|
|
*
|
|
* The `textStyle` object can either be a plain object or an instance of
|
|
* `zrender/src/graphic/Style`, and either be the style of normal or emphasis.
|
|
* After this mothod called, the `textStyle` object can then be used in
|
|
* `el.setStyle(textStyle)` or `el.hoverStyle = textStyle`.
|
|
*
|
|
* Default value will be adopted and `insideRollbackOpt` will be created.
|
|
* See `applyDefaultTextStyle` `rollbackDefaultTextStyle` for more details.
|
|
*
|
|
* opt: {
|
|
* 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;
|
|
|
|
if (opt.getTextPosition) {
|
|
textPosition = opt.getTextPosition(textStyleModel, isEmphasis);
|
|
} else {
|
|
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`.
|
|
// FIXME: consider `label: {formatter: '{a|xx}', color: 'blue', rich: {a: {}}}`,
|
|
// the default color `'blue'` will not be adopted if no color declared in `rich`.
|
|
// That might confuses users. So probably we should put `textStyleModel` as the
|
|
// root ancestor of the `richTextStyle`. But that would be a break change.
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
/**
|
|
* Give some default value to the input `textStyle` object, based on the current settings
|
|
* in this `textStyle` object.
|
|
*
|
|
* The Scenario:
|
|
* when text position is `inside` and `textFill` is not specified, we show
|
|
* text border by default for better view. But it should be considered that text position
|
|
* might be changed when hovering or being emphasis, where the `insideRollback` is used to
|
|
* restore the style.
|
|
*
|
|
* Usage (& NOTICE):
|
|
* When a style object (eithor plain object or instance of `zrender/src/graphic/Style`) is
|
|
* about to be modified on its text related properties, `rollbackDefaultTextStyle` should
|
|
* be called before the modification and `applyDefaultTextStyle` should be called after that.
|
|
* (For the case that all of the text related properties is reset, like `setTextStyleCommon`
|
|
* does, `rollbackDefaultTextStyle` is not needed to be called).
|
|
*/
|
|
|
|
|
|
function applyDefaultTextStyle(textStyle) {
|
|
var textPosition = textStyle.textPosition;
|
|
var opt = textStyle.insideRollbackOpt;
|
|
var insideRollback;
|
|
|
|
if (opt && textStyle.textFill == null) {
|
|
var autoColor = opt.autoColor;
|
|
var isRectText = opt.isRectText;
|
|
var useInsideStyle = opt.useInsideStyle;
|
|
var useInsideStyleCache = useInsideStyle !== false && (useInsideStyle === true || isRectText && textPosition // textPosition can be [10, 30]
|
|
&& typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0);
|
|
var useAutoColorCache = !useInsideStyleCache && autoColor != null; // All of the props declared in `CACHED_LABEL_STYLE_PROPERTIES` are to be cached.
|
|
|
|
if (useInsideStyleCache || useAutoColorCache) {
|
|
insideRollback = {
|
|
textFill: textStyle.textFill,
|
|
textStroke: textStyle.textStroke,
|
|
textStrokeWidth: textStyle.textStrokeWidth
|
|
};
|
|
}
|
|
|
|
if (useInsideStyleCache) {
|
|
textStyle.textFill = '#fff'; // Consider text with #fff overflow its container.
|
|
|
|
if (textStyle.textStroke == null) {
|
|
textStyle.textStroke = autoColor;
|
|
textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2);
|
|
}
|
|
}
|
|
|
|
if (useAutoColorCache) {
|
|
textStyle.textFill = autoColor;
|
|
}
|
|
} // Always set `insideRollback`, so that the previous one can be cleared.
|
|
|
|
|
|
textStyle.insideRollback = insideRollback;
|
|
}
|
|
/**
|
|
* Consider the case: in a scatter,
|
|
* label: {
|
|
* normal: {position: 'inside'},
|
|
* emphasis: {position: 'top'}
|
|
* }
|
|
* In the normal state, the `textFill` will be set as '#fff' for pretty view (see
|
|
* `applyDefaultTextStyle`), but when switching to emphasis state, the `textFill`
|
|
* should be retured to 'autoColor', but not keep '#fff'.
|
|
*/
|
|
|
|
|
|
function rollbackDefaultTextStyle(style) {
|
|
var insideRollback = style.insideRollback;
|
|
|
|
if (insideRollback) {
|
|
// Reset all of the props in `CACHED_LABEL_STYLE_PROPERTIES`.
|
|
style.textFill = insideRollback.textFill;
|
|
style.textStroke = insideRollback.textStroke;
|
|
style.textStrokeWidth = insideRollback.textStrokeWidth;
|
|
style.insideRollback = null;
|
|
}
|
|
}
|
|
|
|
function getFont(opt, ecModel) {
|
|
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 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!'); });
|
|
*/
|
|
|
|
|
|
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 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
|
|
*/
|
|
|
|
|
|
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]
|
|
*/
|
|
|
|
|
|
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]
|
|
*/
|
|
|
|
|
|
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'
|
|
*/
|
|
|
|
|
|
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.
|
|
*/
|
|
|
|
|
|
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.
|
|
*/
|
|
|
|
|
|
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.
|
|
*/
|
|
|
|
|
|
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.
|
|
*/
|
|
|
|
|
|
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');
|
|
}
|
|
}
|
|
/**
|
|
* Return `true` if the given line (line `a`) and the given polygon
|
|
* are intersect.
|
|
* Note that we do not count colinear as intersect here because no
|
|
* requirement for that. We could do that if required in future.
|
|
*
|
|
* @param {number} a1x
|
|
* @param {number} a1y
|
|
* @param {number} a2x
|
|
* @param {number} a2y
|
|
* @param {Array.<Array.<number>>} points Points of the polygon.
|
|
* @return {boolean}
|
|
*/
|
|
|
|
|
|
function linePolygonIntersect(a1x, a1y, a2x, a2y, points) {
|
|
for (var i = 0, p2 = points[points.length - 1]; i < points.length; i++) {
|
|
var p = points[i];
|
|
|
|
if (lineLineIntersect(a1x, a1y, a2x, a2y, p[0], p[1], p2[0], p2[1])) {
|
|
return true;
|
|
}
|
|
|
|
p2 = p;
|
|
}
|
|
}
|
|
/**
|
|
* Return `true` if the given two lines (line `a` and line `b`)
|
|
* are intersect.
|
|
* Note that we do not count colinear as intersect here because no
|
|
* requirement for that. We could do that if required in future.
|
|
*
|
|
* @param {number} a1x
|
|
* @param {number} a1y
|
|
* @param {number} a2x
|
|
* @param {number} a2y
|
|
* @param {number} b1x
|
|
* @param {number} b1y
|
|
* @param {number} b2x
|
|
* @param {number} b2y
|
|
* @return {boolean}
|
|
*/
|
|
|
|
|
|
function lineLineIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
|
|
// let `vec_m` to be `vec_a2 - vec_a1` and `vec_n` to be `vec_b2 - vec_b1`.
|
|
var mx = a2x - a1x;
|
|
var my = a2y - a1y;
|
|
var nx = b2x - b1x;
|
|
var ny = b2y - b1y; // `vec_m` and `vec_n` are parallel iff
|
|
// exising `k` such that `vec_m = k · vec_n`, equivalent to `vec_m X vec_n = 0`.
|
|
|
|
var nmCrossProduct = crossProduct2d(nx, ny, mx, my);
|
|
|
|
if (nearZero(nmCrossProduct)) {
|
|
return false;
|
|
} // `vec_m` and `vec_n` are intersect iff
|
|
// existing `p` and `q` in [0, 1] such that `vec_a1 + p * vec_m = vec_b1 + q * vec_n`,
|
|
// such that `q = ((vec_a1 - vec_b1) X vec_m) / (vec_n X vec_m)`
|
|
// and `p = ((vec_a1 - vec_b1) X vec_n) / (vec_n X vec_m)`.
|
|
|
|
|
|
var b1a1x = a1x - b1x;
|
|
var b1a1y = a1y - b1y;
|
|
var q = crossProduct2d(b1a1x, b1a1y, mx, my) / nmCrossProduct;
|
|
|
|
if (q < 0 || q > 1) {
|
|
return false;
|
|
}
|
|
|
|
var p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct;
|
|
|
|
if (p < 0 || p > 1) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
/**
|
|
* Cross product of 2-dimension vector.
|
|
*/
|
|
|
|
|
|
function crossProduct2d(x1, y1, x2, y2) {
|
|
return x1 * y2 - x2 * y1;
|
|
}
|
|
|
|
function nearZero(val) {
|
|
return val <= 1e-6 && val >= -1e-6;
|
|
} // Register built-in shapes. These shapes might be overwirtten
|
|
// by users, although we do not recommend that.
|
|
|
|
|
|
registerShape('circle', Circle);
|
|
registerShape('sector', Sector);
|
|
registerShape('ring', Ring);
|
|
registerShape('polygon', Polygon);
|
|
registerShape('polyline', Polyline);
|
|
registerShape('rect', Rect);
|
|
registerShape('line', Line);
|
|
registerShape('bezierCurve', BezierCurve);
|
|
registerShape('arc', Arc);
|
|
exports.Z2_EMPHASIS_LIFT = Z2_EMPHASIS_LIFT;
|
|
exports.CACHED_LABEL_STYLE_PROPERTIES = CACHED_LABEL_STYLE_PROPERTIES;
|
|
exports.extendShape = extendShape;
|
|
exports.extendPath = extendPath;
|
|
exports.registerShape = registerShape;
|
|
exports.getShapeClass = getShapeClass;
|
|
exports.makePath = makePath;
|
|
exports.makeImage = makeImage;
|
|
exports.mergePath = mergePath;
|
|
exports.resizePath = resizePath;
|
|
exports.subPixelOptimizeLine = subPixelOptimizeLine;
|
|
exports.subPixelOptimizeRect = subPixelOptimizeRect;
|
|
exports.subPixelOptimize = subPixelOptimize;
|
|
exports.setElementHoverStyle = setElementHoverStyle;
|
|
exports.setHoverStyle = setHoverStyle;
|
|
exports.setAsHighDownDispatcher = setAsHighDownDispatcher;
|
|
exports.isHighDownDispatcher = isHighDownDispatcher;
|
|
exports.getHighlightDigit = getHighlightDigit;
|
|
exports.setLabelStyle = setLabelStyle;
|
|
exports.modifyLabelStyle = modifyLabelStyle;
|
|
exports.setTextStyle = setTextStyle;
|
|
exports.setText = setText;
|
|
exports.getFont = getFont;
|
|
exports.updateProps = updateProps;
|
|
exports.initProps = initProps;
|
|
exports.getTransform = getTransform;
|
|
exports.applyTransform = applyTransform;
|
|
exports.transformDirection = transformDirection;
|
|
exports.groupTransition = groupTransition;
|
|
exports.clipPointsByRect = clipPointsByRect;
|
|
exports.clipRectByRect = clipRectByRect;
|
|
exports.createIcon = createIcon;
|
|
exports.linePolygonIntersect = linePolygonIntersect;
|
|
exports.lineLineIntersect = lineLineIntersect; |