G2Plot 不能重交互?结合 CreatePortal 为所欲为吧

avatar
数据可视化

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>'
  }
],

image.png

从示例可以看出,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;
    }
  }
],

image.png

看上去已经很完美,但业务实际要复杂的多,很简单的一种情况,如果图表容器有 overflow: 'hidden' 的配置,会看到如下效果。

image.png

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;
  };

image.pngimage.png

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' 也不影响,效果如下:

image.png

onmouseleave 等事件,根据业务需求处理就行了。

What's more?

其实类似问题不少,有时候可以绕过,有时则不能,当遇到时可以考虑下 CreatePortal,例如典型的 tooltip ,我们可结合 customContent 迎刃而解。

image.png

示例代码地址: codesandbox.io/s/annotatio…