Preface

G2Plot:一个基于配置、体验优雅、面向数据分析的统计图表库**,**帮助开发者以最小成本绘制高质量统计图表,诞生于阿里经济体 BI 产品真实场景的业务诉求。 CreatePortal:React 官方推荐,Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

React 和 G2Plot 都不了解的同学请先移步官网,如果你熟悉其一,可以往下看了,这篇文章主要介绍怎么将两者结合起来,在 G2Plot 提供的图表上进行富操作。

Annotations

G2Plot 提供 Annotation 作为图表的辅助元素,主要用于在图表上标识额外的标记注解,目前包括 line、text、image、html等10种类型。类型虽多,但每种类型的配置项都有一定的限制,在复杂业务场景显得很鸡肋,毕竟是 canvas 不是 html。
其它几个类型比较简单也没有太多操作空间,以 type: ‘html’ 为例,一个简单的辅助标记如下:

  1. annotations: [
  2. {
  3. type: "html",
  4. position: ["1995", 4.9],
  5. html: '<p>辅助标记</p>'
  6. }
  7. ],

image.png
从示例可以看出,html 类型是支持 html 字符串的,在 React Vue 横行的时代,我相信没有人愿意去拼接 html 字符串了,除非迫不得已。

ReactDOM

处理 html 字符串的方式很多,不赘述。

Render

既然 type: ‘html’ 模式支持 html 字符串,不知你是否想到 ReactDOM,配合 ReactDOM 几乎可以完美实现了,简单改造之后效果如下。

  1. annotations: [
  2. {
  3. type: "html",
  4. position: ["1995", 4.9],
  5. html: () => {
  6. const ele = document.createElement("div");
  7. ReactDom.render(<Annotation />, ele);
  8. return ele;
  9. }
  10. }
  11. ],

image.png

看上去已经很完美,但业务实际要复杂的多,很简单的一种情况,如果图表容器有 overflow: ‘hidden’ 的配置,会看到如下效果。
image.png
Annotation 被截断了,增加容器高度或是 Annotation 组件添加滚动条,往往都不是最佳解法。

CreatePortal

为了解决上述问题,让 Annotation 不受限于父容器,我们可以借助 CreatePortal 将 Annotation 渲染到任意我们期望的 DOM 树上,以 body 为例。

  1. const getAnnotationHtml = () => {
  2. const ele = document.createElement("div");
  3. ele.id = "annotation-box";
  4. ReactDOM.render(
  5. <>
  6. {ReactDOM.createPortal(
  7. <Annotation />,
  8. document.getElementsByTagName("body")[0]
  9. )}
  10. </>,
  11. ele
  12. );
  13. return ele;
  14. };

image.pngimage.png

Annotation 正确渲染在 body 里面了,但并不是我们期望的效果,因为 Annotaion 没有渲染在 HTMLElement id[‘annotation-box’] 里面, 所以位置偏离了。
其实正常情况下,如果 Annotation 的内容过多,也不宜直接展示,因为太太太遮挡内容了,我们简单的添加个交互(onmousemove)。

  1. // 全量代码请查看示例代码
  2. annotations: [
  3. {
  4. type: "html",
  5. position: ["1995", 4.9],
  6. html: getAnnotationHtml()
  7. }
  8. ]
  9. const getPosition = (targetElement) => {
  10. const { top, left, right } = targetElement.getBoundingClientRect();
  11. // 需要考虑有滚动条时的情况
  12. const boxTop =
  13. top -
  14. document.documentElement.clientTop +
  15. document.documentElement.scrollTop;
  16. const boxLeft =
  17. left -
  18. document.documentElement.clientLeft +
  19. document.documentElement.scrollLeft;
  20. const body = document.getElementsByTagName("body")[0];
  21. const { width } = body.getBoundingClientRect();
  22. const boxWdith = 230; // 容器宽度
  23. const offsetX = width - right < boxWdith ? boxWdith - (width - right) : 0; // 考虑超出右侧的情况
  24. return {
  25. left: boxLeft - offsetX,
  26. top: boxTop
  27. };
  28. };
  29. const showAnnotationComponent = (e) => {
  30. const exist = document.querySelector("#annotaion-component");
  31. if (!exist) {
  32. const targetElement = e.currentTarget.parentNode.getElementsByClassName(
  33. "annotation-box"
  34. )[0];
  35. const { top, left } = getPosition(targetElement);
  36. ReactDOM.render(
  37. <>
  38. {ReactDOM.createPortal(
  39. <div
  40. id="annotaion-component"
  41. style={{ position: "absolute", left, top }}
  42. >
  43. <Annotation />
  44. </div>,
  45. document.getElementsByTagName("body")[0]
  46. )}
  47. </>,
  48. targetElement
  49. );
  50. } else {
  51. // todo set display
  52. }
  53. };
  54. const getAnnotationHtml = () => {
  55. const ele = document.createElement("div");
  56. ele.innerHTML = '<span>查看详情</span><div class="annotation-box"></div>';
  57. ele.onmousemove = (e) => {
  58. showAnnotationComponent(e);
  59. };
  60. return ele;
  61. };

当鼠标移动到查看详情上时,即使父容器设置了 overFlow: ‘hidden’ 也不影响,效果如下:
image.png
onmouseleave 等事件,根据业务需求处理就行了。

What’s more?

其实类似问题不少,有时候可以绕过,有时则不能,当遇到时可以考虑下 CreatePortal,例如典型的 tooltip ,我们可结合 customContent 迎刃而解。
image.png
示例代码地址: https://codesandbox.io/s/annotation-create-portal-n8enp?file=/App.tsx