const Util = require('../util'); const DomUtil = Util.DomUtil; const Component = require('../component'); const positionAdjust = require('./utils/position-adjust'); const spirialAdjust = require('./utils/spiral-adjust'); const bboxAdjust = require('./utils/bbox-adjust'); const LAYOUTS = { scatter: positionAdjust, map: spirialAdjust, treemap: bboxAdjust }; class Label extends Component { getDefaultCfg() { const cfg = super.getDefaultCfg(); return Util.mix({}, cfg, { name: 'label', /** * label类型 * @type {(String)} */ type: 'default', /** * 默认文本样式 * @type {Array} */ textStyle: null, /** * 文本显示格式化回调函数 * @type {Function} */ formatter: null, /** * 显示的文本集合 * @type {Array} */ items: null, /** * 是否使用html渲染label * @type {String} */ useHtml: false, /** * html 渲染时用的容器的模板,必须存在 class = "g-labels" * @type {String} */ containerTpl: '
', /** * html 渲染时单个 label 的模板,必须存在 class = "g-label" * @type {String} */ itemTpl: '
{text}
', /** * label牵引线定义 * @type {String || Object} */ labelLine: false, /** * label牵引线容器 * @type Object */ lineGroup: null, /** * 需添加label的shape * @type Object */ shapes: null, /** * 默认为true。为false时指定直接用items渲染文本,不进行config * @type Object */ config: true, /** * 是否进行拾取 * @type Object */ capture: true }); } /* * 清空label容器 */ clear() { const group = this.get('group'); const container = this.get('container'); if (group && !group.get('destroyed')) { group.clear(); } if (container) { container.innerHTML = ''; } super.clear(); } /** * 销毁group */ destroy() { const group = this.get('group'); const container = this.get('container'); if (!group.destroy) { group.destroy(); } if (container) { container.parentNode && container.parentNode.removeChild(container); } super.destroy(); // 要最后调用 super.destroy 否则 get 属性会无效 } /** * label绘制全过程 */ render() { this.clear(); this._init(); this.beforeDraw(); this.draw(); this.afterDraw(); } _dryDraw() { const self = this; const items = self.get('items'); const children = self.getLabels(); const count = children.length; Util.each(items, (item, index) => { if (index < count) { const label = children[index]; self.changeLabel(label, item); } else { const labelShape = self._addLabel(item, index); if (labelShape) { labelShape._id = item._id; labelShape.set('coord', item.coord); } } }); for (let i = count - 1; i >= items.length; i--) { children[i].remove(); } self._adjustLabels(); if (self.get('labelLine') || (!self.get('config'))) { self.drawLines(); } } /** * 更新label * 1. 将items与group中的children对比,更新/新增/删除labels * 2. labels布局优化 * 3. 画label连接线 * 4. 绘制到画布 */ draw() { this._dryDraw(); this.get('canvas').draw(); } /* * 更新label * oldLabel shape或label dom * newLabel label data * index items中的下标 */ changeLabel(oldLabel, newLabel) { if (!oldLabel) { return; } if (oldLabel.tagName) { const node = this._createDom(newLabel); oldLabel.innerHTML = node.innerHTML; this._setCustomPosition(newLabel, oldLabel); } else { oldLabel._id = newLabel._id; oldLabel.attr('text', newLabel.text); if (oldLabel.attr('x') !== newLabel.x || oldLabel.attr('y') !== newLabel.y) { oldLabel.resetMatrix(); if (newLabel.textStyle.rotate) { oldLabel.rotateAtStart(newLabel.textStyle.rotate); delete newLabel.textStyle.rotate; } oldLabel.attr(newLabel); } } } /** * 显示label */ show() { const group = this.get('group'); const container = this.get('container'); if (group) { group.show(); } if (container) { container.style.opacity = 1; } } /** * 隐藏label */ hide() { const group = this.get('group'); const container = this.get('container'); if (group) { group.hide(); } if (container) { container.style.opacity = 0; } } /** * 画label连接线 */ drawLines() { const self = this; const lineStyle = self.get('labelLine'); if (typeof lineStyle === 'boolean') { self.set('labelLine', {}); } let lineGroup = self.get('lineGroup'); if (!lineGroup || lineGroup.get('destroyed')) { lineGroup = self.get('group').addGroup({ elCls: 'x-line-group' }); self.set('lineGroup', lineGroup); } else { lineGroup.clear(); } Util.each(self.get('items'), label => { self.lineToLabel(label, lineGroup); }); } lineToLabel(label, lineGroup) { const self = this; if (!self.get('config') && !label.labelLine) { return; } const lineStyle = label.labelLine || self.get('labelLine'); const capture = typeof label.capture === 'undefined' ? self.get('capture') : label.capture; let path = lineStyle.path; if (path && Util.isFunction(lineStyle.path)) { path = lineStyle.path(label); } if (!path) { const start = label.start || { x: label.x - label._offset.x, y: label.y - label._offset.y }; path = [ [ 'M', start.x, start.y ], [ 'L', label.x, label.y ] ]; } let stroke = label.color; if (!stroke) { if (label.textStyle && label.textStyle.fill) { stroke = label.textStyle.fill; } else { stroke = '#000'; } } const lineShape = lineGroup.addShape('path', { attrs: Util.mix({ path, fill: null, stroke }, lineStyle), capture }); // label 对应线的动画关闭 lineShape.name = self.get('name'); // generate labelLine id according to label id lineShape._id = label._id && label._id.replace('glabel', 'glabelline'); lineShape.set('coord', self.get('coord')); } // 根据type对label布局 _adjustLabels() { const self = this; const type = self.get('type'); const labels = self.getLabels(); const shapes = self.get('shapes'); const layout = LAYOUTS[type]; if (type === 'default' || !layout) { return; } layout(labels, shapes); } /** * 获取当前所有label实例 * @return {Array} 当前label实例 */ getLabels() { const container = this.get('container'); if (container) { return Util.toArray(container.childNodes); } return this.get('group').get('children'); } // 先计算label的所有配置项,然后生成label实例 _addLabel(item, index) { let cfg = item; if (this.get('config')) { cfg = this._getLabelCfg(item, index); } return this._createText(cfg); } _getLabelCfg(item, index) { let textStyle = this.get('textStyle') || {}; const formatter = this.get('formatter'); const htmlTemplate = this.get('htmlTemplate'); if (!Util.isObject(item)) { const tmp = item; item = {}; item.text = tmp; } if (Util.isFunction(textStyle)) { textStyle = textStyle(item.text, item, index); } if (formatter) { item.text = formatter(item.text, item, index); } if (htmlTemplate) { item.useHtml = true; if (Util.isFunction(htmlTemplate)) { item.text = htmlTemplate(item.text, item, index); } } if (Util.isNil(item.text)) { item.text = ''; } item.text = item.text + ''; // ? 为什么转换为字符串 const cfg = Util.mix({}, item, { textStyle }, { x: item.x || 0, y: item.y || 0 }); return cfg; } /** * label初始化,主要针对html容器 */ _init() { if (!this.get('group')) { const group = this.get('canvas').addGroup({ id: 'label-group' }); this.set('group', group); } } initHtmlContainer() { let container = this.get('container'); if (!container) { const containerTpl = this.get('containerTpl'); const wrapper = this.get('canvas').get('el').parentNode; container = DomUtil.createDom(containerTpl); wrapper.style.position = 'relative'; wrapper.appendChild(container); this.set('container', container); } else if (Util.isString(container)) { container = document.getElementById(container); if (container) { this.set('container', container); } } return container; } // 分html dom和G shape两种情况生成label实例 _createText(config) { // @2018-11-29 by blue.lb 这里由于使用delete导致之后的配置无法获取到point和rotate,出现问题,深拷贝一次比较好 let cfg = Util.deepMix({}, config); let container = this.get('container'); const capture = typeof cfg.capture === 'undefined' ? this.get('capture') : cfg.capture; let labelShape; if (cfg.useHtml || cfg.htmlTemplate) { if (!container) { container = this.initHtmlContainer(); } const node = this._createDom(cfg); container.appendChild(node); this._setCustomPosition(cfg, node); } else { const name = this.get('name'); const origin = cfg.point; const group = this.get('group'); delete cfg.point; // 临时解决,否则影响动画 let rotate = cfg.rotate; // textStyle中的rotate虽然可以正常画出,但是在做动画的时候可能会导致动画异常。移出,在定义好shape后通过transform实现效果。 if (cfg.textStyle) { if (cfg.textStyle.rotate) { rotate = cfg.textStyle.rotate; delete cfg.textStyle.rotate; } cfg = Util.mix({ x: cfg.x, y: cfg.y, textAlign: cfg.textAlign, text: cfg.text }, cfg.textStyle); } labelShape = group.addShape('text', { attrs: cfg, capture }); if (rotate) { // rotate是用角度定义的,转换为弧度 if (Math.abs(rotate) > Math.PI * 2) { rotate = rotate / 180 * Math.PI; } labelShape.transform([ [ 't', -cfg.x, -cfg.y ], [ 'r', rotate ], [ 't', cfg.x, cfg.y ] ]); } labelShape.setSilent('origin', origin || cfg); labelShape.name = name; // 用于事件标注 this.get('appendInfo') && labelShape.setSilent('appendInfo', this.get('appendInfo')); return labelShape; } } _createDom(cfg) { const itemTpl = this.get('itemTpl'); const str = Util.substitute(itemTpl, { text: cfg.text }); return DomUtil.createDom(str); } // 根据文本对齐方式确定dom位置 _setCustomPosition(cfg, htmlDom) { const textAlign = cfg.textAlign || 'left'; let top = cfg.y; let left = cfg.x; const width = DomUtil.getOuterWidth(htmlDom); const height = DomUtil.getOuterHeight(htmlDom); top = top - height / 2; if (textAlign === 'center') { left = left - width / 2; } else if (textAlign === 'right') { left = left - width; } htmlDom.style.top = parseInt(top, 10) + 'px'; htmlDom.style.left = parseInt(left, 10) + 'px'; } } module.exports = Label;