in src/chart/treemap/TreemapView.ts [763:1141]
function renderNode(
seriesModel: TreemapSeriesModel,
thisStorage: RenderElementStorage,
oldStorage: RenderElementStorage,
reRoot: ReRoot,
lastsForAnimation: RenderResult['lastsForAnimation'],
willInvisibleEls: RenderResult['willInvisibleEls'],
thisNode: TreeNode,
oldNode: TreeNode,
parentGroup: graphic.Group,
depth: number
) {
// Whether under viewRoot.
if (!thisNode) {
// Deleting nodes will be performed finally. This method just find
// element from old storage, or create new element, set them to new
// storage, and set styles.
return;
}
// -------------------------------------------------------------------
// Start of closure variables available in "Procedures in renderNode".
const thisLayout = thisNode.getLayout();
const data = seriesModel.getData();
const nodeModel = thisNode.getModel<TreemapSeriesNodeItemOption>();
// Only for enabling highlight/downplay. Clear firstly.
// Because some node will not be rendered.
data.setItemGraphicEl(thisNode.dataIndex, null);
if (!thisLayout || !thisLayout.isInView) {
return;
}
const thisWidth = thisLayout.width;
const thisHeight = thisLayout.height;
const borderWidth = thisLayout.borderWidth;
const thisInvisible = thisLayout.invisible;
const thisRawIndex = thisNode.getRawIndex();
const oldRawIndex = oldNode && oldNode.getRawIndex();
const thisViewChildren = thisNode.viewChildren;
const upperHeight = thisLayout.upperHeight;
const isParent = thisViewChildren && thisViewChildren.length;
const itemStyleNormalModel = nodeModel.getModel('itemStyle');
const itemStyleEmphasisModel = nodeModel.getModel(['emphasis', 'itemStyle']);
const itemStyleBlurModel = nodeModel.getModel(['blur', 'itemStyle']);
const itemStyleSelectModel = nodeModel.getModel(['select', 'itemStyle']);
const borderRadius = itemStyleNormalModel.get('borderRadius') || 0;
// End of closure ariables available in "Procedures in renderNode".
// -----------------------------------------------------------------
// Node group
const group = giveGraphic('nodeGroup', Group);
if (!group) {
return;
}
parentGroup.add(group);
// x,y are not set when el is above view root.
group.x = thisLayout.x || 0;
group.y = thisLayout.y || 0;
group.markRedraw();
inner(group).nodeWidth = thisWidth;
inner(group).nodeHeight = thisHeight;
if (thisLayout.isAboveViewRoot) {
return group;
}
// Background
const bg = giveGraphic('background', Rect, depth, Z2_BG);
bg && renderBackground(group, bg, isParent && thisLayout.upperLabelHeight);
const emphasisModel = nodeModel.getModel('emphasis');
const focus = emphasisModel.get('focus');
const blurScope = emphasisModel.get('blurScope');
const isDisabled = emphasisModel.get('disabled');
const focusOrIndices =
focus === 'ancestor' ? thisNode.getAncestorsIndices()
: focus === 'descendant' ? thisNode.getDescendantIndices()
: focus;
// No children, render content.
if (isParent) {
// Because of the implementation about "traverse" in graphic hover style, we
// can not set hover listener on the "group" of non-leaf node. Otherwise the
// hover event from the descendents will be listenered.
if (isHighDownDispatcher(group)) {
setAsHighDownDispatcher(group, false);
}
if (bg) {
setAsHighDownDispatcher(bg, !isDisabled);
// Only for enabling highlight/downplay.
data.setItemGraphicEl(thisNode.dataIndex, bg);
enableHoverFocus(bg, focusOrIndices, blurScope);
}
}
else {
const content = giveGraphic('content', Rect, depth, Z2_CONTENT);
content && renderContent(group, content);
(bg as ECElement).disableMorphing = true;
if (bg && isHighDownDispatcher(bg)) {
setAsHighDownDispatcher(bg, false);
}
setAsHighDownDispatcher(group, !isDisabled);
// Only for enabling highlight/downplay.
data.setItemGraphicEl(thisNode.dataIndex, group);
const cursorStyle = nodeModel.getShallow('cursor');
cursorStyle && content.attr('cursor', cursorStyle);
enableHoverFocus(group, focusOrIndices, blurScope);
}
return group;
// ----------------------------
// | Procedures in renderNode |
// ----------------------------
function renderBackground(group: graphic.Group, bg: graphic.Rect, useUpperLabel: boolean) {
const ecData = getECData(bg);
// For tooltip.
ecData.dataIndex = thisNode.dataIndex;
ecData.seriesIndex = seriesModel.seriesIndex;
bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight, r: borderRadius});
if (thisInvisible) {
// If invisible, do not set visual, otherwise the element will
// change immediately before animation. We think it is OK to
// remain its origin color when moving out of the view window.
processInvisible(bg);
}
else {
bg.invisible = false;
const style = thisNode.getVisual('style') as PathStyleProps;
const visualBorderColor = style.stroke;
const normalStyle = getItemStyleNormal(itemStyleNormalModel);
normalStyle.fill = visualBorderColor;
const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
emphasisStyle.fill = itemStyleEmphasisModel.get('borderColor');
const blurStyle = getStateItemStyle(itemStyleBlurModel);
blurStyle.fill = itemStyleBlurModel.get('borderColor');
const selectStyle = getStateItemStyle(itemStyleSelectModel);
selectStyle.fill = itemStyleSelectModel.get('borderColor');
if (useUpperLabel) {
const upperLabelWidth = thisWidth - 2 * borderWidth;
prepareText(
// PENDING: convert ZRColor to ColorString for text.
bg, visualBorderColor as ColorString, style.opacity,
{x: borderWidth, y: 0, width: upperLabelWidth, height: upperHeight}
);
}
// For old bg.
else {
bg.removeTextContent();
}
bg.setStyle(normalStyle);
bg.ensureState('emphasis').style = emphasisStyle;
bg.ensureState('blur').style = blurStyle;
bg.ensureState('select').style = selectStyle;
setDefaultStateProxy(bg);
}
group.add(bg);
}
function renderContent(group: graphic.Group, content: graphic.Rect) {
const ecData = getECData(content);
// For tooltip.
ecData.dataIndex = thisNode.dataIndex;
ecData.seriesIndex = seriesModel.seriesIndex;
const contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
const contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
content.culling = true;
content.setShape({
x: borderWidth,
y: borderWidth,
width: contentWidth,
height: contentHeight,
r: borderRadius
});
if (thisInvisible) {
// If invisible, do not set visual, otherwise the element will
// change immediately before animation. We think it is OK to
// remain its origin color when moving out of the view window.
processInvisible(content);
}
else {
content.invisible = false;
const nodeStyle = thisNode.getVisual('style') as PathStyleProps;
const visualColor = nodeStyle.fill;
const normalStyle = getItemStyleNormal(itemStyleNormalModel);
normalStyle.fill = visualColor;
normalStyle.decal = nodeStyle.decal;
const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
const blurStyle = getStateItemStyle(itemStyleBlurModel);
const selectStyle = getStateItemStyle(itemStyleSelectModel);
// PENDING: convert ZRColor to ColorString for text.
prepareText(content, visualColor as ColorString, nodeStyle.opacity, null);
content.setStyle(normalStyle);
content.ensureState('emphasis').style = emphasisStyle;
content.ensureState('blur').style = blurStyle;
content.ensureState('select').style = selectStyle;
setDefaultStateProxy(content);
}
group.add(content);
}
function processInvisible(element: graphic.Rect) {
// Delay invisible setting utill animation finished,
// avoid element vanish suddenly before animation.
!element.invisible && willInvisibleEls.push(element);
}
function prepareText(
rectEl: graphic.Rect,
visualColor: ColorString,
visualOpacity: number,
// Can be null/undefined
upperLabelRect: RectLike
) {
const normalLabelModel = nodeModel.getModel(
upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL
);
const defaultText = convertOptionIdName(nodeModel.get('name'), null);
const isShow = normalLabelModel.getShallow('show');
setLabelStyle(
rectEl,
getLabelStatesModels(nodeModel, upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL),
{
defaultText: isShow ? defaultText : null,
inheritColor: visualColor,
defaultOpacity: visualOpacity,
labelFetcher: seriesModel,
labelDataIndex: thisNode.dataIndex
}
);
const textEl = rectEl.getTextContent();
if (!textEl) {
return;
}
const textStyle = textEl.style;
const textPadding = normalizeCssArray(textStyle.padding || 0);
if (upperLabelRect) {
rectEl.setTextConfig({
layoutRect: upperLabelRect
});
(textEl as ECElement).disableLabelLayout = true;
}
textEl.beforeUpdate = function () {
const width = Math.max(
(upperLabelRect ? upperLabelRect.width : rectEl.shape.width) - textPadding[1] - textPadding[3], 0
);
const height = Math.max(
(upperLabelRect ? upperLabelRect.height : rectEl.shape.height) - textPadding[0] - textPadding[2], 0
);
if (textStyle.width !== width || textStyle.height !== height) {
textEl.setStyle({
width,
height
});
}
};
textStyle.truncateMinChar = 2;
textStyle.lineOverflow = 'truncate';
addDrillDownIcon(textStyle, upperLabelRect, thisLayout);
const textEmphasisState = textEl.getState('emphasis');
addDrillDownIcon(textEmphasisState ? textEmphasisState.style : null, upperLabelRect, thisLayout);
}
function addDrillDownIcon(style: TextStyleProps, upperLabelRect: RectLike, thisLayout: any) {
const text = style ? style.text : null;
if (!upperLabelRect && thisLayout.isLeafRoot && text != null) {
const iconChar = seriesModel.get('drillDownIcon', true);
style.text = iconChar ? iconChar + ' ' + text : text;
}
}
function giveGraphic<T extends graphic.Group | graphic.Rect>(
storageName: keyof RenderElementStorage,
Ctor: {new(): T},
depth?: number,
z?: number
): T {
let element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
const lasts = lastsForAnimation[storageName];
if (element) {
// Remove from oldStorage
oldStorage[storageName][oldRawIndex] = null;
prepareAnimationWhenHasOld(lasts, element);
}
// If invisible and no old element, do not create new element (for optimizing).
else if (!thisInvisible) {
element = new Ctor();
if (element instanceof Displayable) {
element.z2 = calculateZ2(depth, z);
}
prepareAnimationWhenNoOld(lasts, element);
}
// Set to thisStorage
return (thisStorage[storageName][thisRawIndex] = element) as T;
}
function prepareAnimationWhenHasOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
if (element instanceof Group) {
lastCfg.oldX = element.x;
lastCfg.oldY = element.y;
}
else {
lastCfg.oldShape = extend({}, element.shape);
}
}
// If a element is new, we need to find the animation start point carefully,
// otherwise it will looks strange when 'zoomToNode'.
function prepareAnimationWhenNoOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
const parentNode = thisNode.parentNode;
const isGroup = element instanceof graphic.Group;
if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
let parentOldX = 0;
let parentOldY = 0;
// New nodes appear from right-bottom corner in 'zoomToNode' animation.
// For convenience, get old bounding rect from background.
const parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
if (!reRoot && parentOldBg && parentOldBg.oldShape) {
parentOldX = parentOldBg.oldShape.width;
parentOldY = parentOldBg.oldShape.height;
}
// When no parent old shape found, its parent is new too,
// so we can just use {x:0, y:0}.
if (isGroup) {
lastCfg.oldX = 0;
lastCfg.oldY = parentOldY;
}
else {
lastCfg.oldShape = {x: parentOldX, y: parentOldY, width: 0, height: 0};
}
}
// Fade in, user can be aware that these nodes are new.
lastCfg.fadein = !isGroup;
}
}