Preface
G2Plot:一个基于配置、体验优雅、面向数据分析的统计图表库,帮助开发者以最小成本绘制高质量统计图表,诞生于阿里经济体 BI 产品真实场景的业务诉求。CreatePortal:React 官方推荐,Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
React 和 G2Plot 都不了解的同学请先移步官网,如果你熟悉其一,可以往下看了,这篇文章主要介绍怎么将两者结合起来,在 G2Plot 提供的图表上进行富操作。
Annotations
G2Plot 提供 Annotation 作为图表的辅助元素,主要用于在图表上标识额外的标记注解,目前包括 line、text、image、html等10种类型。类型虽多,但每种类型的配置项都有一定的限制,在复杂业务场景显得很鸡肋,毕竟是 canvas 不是 html。
其它几个类型比较简单也没有太多操作空间,以 type: 'html' 为例,一个简单的辅助标记如下:
annotations: [ { type: "html", position: ["1995", 4.9], html: '<p>辅助标记</p>' } ],
从示例可以看出,html 类型是支持 html 字符串的,在 React Vue 横行的时代,我相信没有人愿意去拼接 html 字符串了,除非迫不得已。
ReactDOM
处理 html 字符串的方式很多,不赘述。
Render
既然 type: 'html' 模式支持 html 字符串,不知你是否想到 ReactDOM,配合 ReactDOM 几乎可以完美实现了,简单改造之后效果如下。
annotations: [ { type: "html", position: ["1995", 4.9], html: () => { const ele = document.createElement("div"); ReactDom.render(<Annotation />, ele); return ele; } } ],
看上去已经很完美,但业务实际要复杂的多,很简单的一种情况,如果图表容器有 overflow: 'hidden' 的配置,会看到如下效果。
Annotation 被截断了,增加容器高度或是 Annotation 组件添加滚动条,往往都不是最佳解法。
CreatePortal
为了解决上述问题,让 Annotation 不受限于父容器,我们可以借助 CreatePortal 将 Annotation 渲染到任意我们期望的 DOM 树上,以 body 为例。
const getAnnotationHtml = () => { const ele = document.createElement("div"); ele.id = "annotation-box"; ReactDOM.render( <> {ReactDOM.createPortal( <Annotation />, document.getElementsByTagName("body")[0] )} </>, ele ); return ele; };
Annotation 正确渲染在 body 里面了,但并不是我们期望的效果,因为 Annotaion 没有渲染在 HTMLElement id['annotation-box'] 里面, 所以位置偏离了。
其实正常情况下,如果 Annotation 的内容过多,也不宜直接展示,因为太太太遮挡内容了,我们简单的添加个交互(onmousemove)。
// 全量代码请查看示例代码 annotations: [ { type: "html", position: ["1995", 4.9], html: getAnnotationHtml() } ] const getPosition = (targetElement) => { const { top, left, right } = targetElement.getBoundingClientRect(); // 需要考虑有滚动条时的情况 const boxTop = top - document.documentElement.clientTop + document.documentElement.scrollTop; const boxLeft = left - document.documentElement.clientLeft + document.documentElement.scrollLeft; const body = document.getElementsByTagName("body")[0]; const { width } = body.getBoundingClientRect(); const boxWdith = 230; // 容器宽度 const offsetX = width - right < boxWdith ? boxWdith - (width - right) : 0; // 考虑超出右侧的情况 return { left: boxLeft - offsetX, top: boxTop }; }; const showAnnotationComponent = (e) => { const exist = document.querySelector("#annotaion-component"); if (!exist) { const targetElement = e.currentTarget.parentNode.getElementsByClassName( "annotation-box" )[0]; const { top, left } = getPosition(targetElement); ReactDOM.render( <> {ReactDOM.createPortal( <div id="annotaion-component" style={{ position: "absolute", left, top }} > <Annotation /> </div>, document.getElementsByTagName("body")[0] )} </>, targetElement ); } else { // todo set display } }; const getAnnotationHtml = () => { const ele = document.createElement("div"); ele.innerHTML = '<span>查看详情</span><div class="annotation-box"></div>'; ele.onmousemove = (e) => { showAnnotationComponent(e); }; return ele; };
当鼠标移动到查看详情上时,即使父容器设置了 overFlow: 'hidden' 也不影响,效果如下:
onmouseleave 等事件,根据业务需求处理就行了。
What's more?
其实类似问题不少,有时候可以绕过,有时则不能,当遇到时可以考虑下 CreatePortal,例如典型的 tooltip ,我们可结合 customContent 迎刃而解。
示例代码地址: https://codesandbox.io/s/annotation-create-portal-n8enp?file=/App.tsx
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。