Echarts 如何框选对象

530 阅读5分钟

对象框选

基本定义

本文对 框选对象 的定义是指:用户可以通过 点击双击 等操作,用自定义矩形框,选中 Echarts 图表内的 图形元素

众所周知,Echarts 本身并未暴露 直接框选对象 的能力。这也就是本文的价值所在:基于 Echarts 实现 创新性拓展

本文重点介绍如何灵活框选 标题坐标轴图例 三大图形元素对象。

聪明的你,很容易就 预见性 地判断出,实现对象框选的主要难点在于:

  1. 灵活识别框选对象
  2. 动态更新框选尺寸
  3. 跟随内容更新方框

主要特点

对象框选将具备以下特点:

  1. 根据用户点击处位置,自动推算框选的对象
  2. 当框选对象内容变化,选中框自动改变尺寸
  3. 当框选对象位置变化,选中框自动跟随行动
  4. 当画布容器尺寸变化,选中框自动更新大小
  5. 选中框对象本身唯一,框选对象间存在互斥
  6. 选中对象具备优先级,标题>图例>X轴>Y轴

触发时机

  1. 单击对象区域
  2. 画布容器变化
  3. 对象配置更新

效果演示

👉👉👉 在线演示 👈👈👈

e2o9j8mMhb.gif

认识 ZRender

ZRender 是二维绘图引擎,它提供 Canvas、SVG、VML 等多种渲染方式。

它也是 ECharts 的渲染器。

chrome_OaG8NJpwuF.gif

我们可以通过 ZRender 直接拓展 Echarts 的能力,如本文涉及的 对象框选 能力!

// 向画布中添加图形元素
export function addGraphic(graphic, zRender) {
  if (!graphic || !zRender) return

  zRender.add(graphic)
}

// 获取画布尺寸
export function getZRenderSize(echarts) {
  const zRender = echarts.getZr()

  return {
    width: zRender.getWidth(),
    height: zRender.getHeight()
  }
}

通用工具

工具常量

  1. 公共常量: 矩形元素占位配置
export const RECT_PLACEHOLDER = {
  x: 0,
  y: 0,
  width: 0, // BoundingRect
  height: 0, // BoundingRect
  shape: {
    width: 0,
    height: 0,
    x: 0,
    y: 0,
    r: 0,
  }
}
  1. 公共常量: 标题唯一标识前缀
 // 画布唯一标识
export const GRAPH_UNIQUE = 'my-graph-unique'
 // 标题唯一标识
export const TITLE_UNIQUE = 'my-title-unique'
 // 图例唯一标识
export const LEGEND_UNIQUE = 'my-legend-unique'
 // X轴唯一标识
export const X_AXIS_UNIQUE = 'my-xAxis-unique'
 // Y轴唯一标识
export const Y_AXIS_UNIQUE = 'my-yAxis-unique'
  1. 公共常量: 矩形元素圆角值
export const RECT_RADIUS = 2
  1. 公共常量: 框框对象枚举值
// 点击目标信息
export const MOUSE_TARGET_ENUM = {
  BLANK: 0, // 空白
  TITLE: 1, // 标题
  LEGEND: 2, // 图例
  XAXIS: 3, // x轴
  YAXIS: 4, // y轴
  VIEW: 5, // 绘图区域
}
  1. 公共常量: 多个坐标轴的固定偏移量
// 坐标轴 offset 差值(同时也是坐标轴选中框的固宽或固高)
export const AXIS_OFFSET_DIFF = 60

工具方法

  1. 工具方法: 根据 Echarts 实例 echartsInstance 移除旧选中框(即反选矩形选中框)
export function unselectRect(echartsInstance, reset = true) {
  if (!echartsInstance) return

  const zRender = echartsInstance.getZr() // 画布
  zRender.remove(RectSelection) // 移除矩形选中框

  if (reset) {
    RectInfo = {}
    RectSelection = null
  }
}
  1. 工具方法: 根据 矩形配置 创建矩形图形元素 rect
export function createRectGraphic(RectOpts) {
  if (!RectOpts) return

  return new echarts.graphic.Rect(RectOpts)
}
  1. 工具方法: 向画布中添加图形元素 graphic
export function addGraphic(graphic, zRender) {
  if (!graphic || !zRender) return

  zRender.add(graphic)
}
  1. 工具方法: 选中框公共样式 graphic
export function getSelectionStyle() {
  return {
    fill: 'transparent',
    stroke: '#E6A23C',
    lineWidth: 1,
    opacity: 0.6,
  }
}
  1. 工具方法: 获取直角坐标系动态矩形 rect
export function getGridRect(echartsInstance) {
  if (!echartsInstance) return RECT_PLACEHOLDER

  try { // 这里 getComponent 入参 xAxis 和 yAxis 无差异
    return echartsInstance.getModel().getComponent('yAxis').axis.grid.getRect()
    || RECT_PLACEHOLDER
  } catch (e) {
    return RECT_PLACEHOLDER
  }
}
  1. 工具方法: 解决像素位置导致的线条太细时,颜色无法生效
import XEUtils from 'xe-utils'

export function toFixedPixel(pixel, diff = 0.5) {
  return XEUtils.add(XEUtils.floor(pixel), diff) // XEUtils.floor 为向下取整
}

框选标题

  1. 设计方法: 根据 Echarts 实例 echartsInstance 获取标题元素 rect
function getTitleView(echartsInstance) {
  if (!echartsInstance) return RECT_PLACEHOLDER

  try {
    const titleView = echartsInstance._componentsViews.find((c) => 
      c.type === 'title' && c.__id && c.__id.includes(TITLE_UNIQUE)
    )

    const { x = 0, y = 0, _children = [] } = get(titleView, 'group') || {}
    const rect = _children.find((child) => child.type === 'rect')

    if (!rect) return RECT_PLACEHOLDER

    return {
      x,
      y,
      shape: rect.shape || {}
    }
  } catch (e) {
    return RECT_PLACEHOLDER
  }
}
  1. 设计方法: 根据 zRender 获取标题选中框元素 rect
function getTitleSelection(zRender, echartsInstance) {
  const width = zRender.getWidth() - 265 // 根据画布宽度推算选中框宽度
  const { shape } = getTitleView(echartsInstance)
  const shapeHeight = toFixedPixel(shape.height, 0) // 标题选中框高度

  return {
    id: `${TITLE_UNIQUE}-selection`,
    x: toFixedPixel(150), // 计算:大概为图形印戳的占位
    y: toFixedPixel(10),
    shape: {
      r: RECT_RADIUS,
      width: toFixedPixel(width, 0),
      height: shapeHeight ? shapeHeight + 20 : 0,
    },
    ignore: !shapeHeight, // 高度为空时,忽略
    style: getSelectionStyle(),
    onclick: () => unselectRect(echartsInstance) // 取消选中
  }
}
  1. 暴露方法: 根据 Echarts 实例及业务配置,框选标题对象
let RectInfo = {} // 矩形选中对象信息
let RectSelection = null // 矩形选中框

export function selectTitle(echarts, TitleConfig = {}) {
  if (!echarts) return
  if (get(TitleConfig, 'closeSelection')) return // 关闭框选时,退出

  unselectRect(echarts) // 移除旧选中框

  const zRender = echarts.getZr() // 画布
  const rectOpts = getTitleSelection(zRender, echarts) // 矩形

  RectInfo.type = MOUSE_TARGET_ENUM.TITLE // 选中对象-标题
  RectSelection = createRectGraphic(rectOpts) // 创建标题选中框
  addGraphic(RectSelection, zRender) // 向画布添加标题选中框
}

框选图例

  1. 设计方法: 根据 zRender 获取图例选中框元素 rect
function getLegendSelection(echartsInstance) {
  const { x, y, shape } = getLegendView(echartsInstance)

  return {
    id: `${LEGEND_UNIQUE}-selection`,
    x: toFixedPixel(x + shape.x - 5),
    y: toFixedPixel(y + shape.y - 5),
    shape: {
      r: RECT_RADIUS,
      width: toFixedPixel(shape.width, 0) + 10,
      height: toFixedPixel(shape.height, 0) + 10,
    },
    style: getSelectionStyle(),
    draggable: false, // 图形元素是否可以被拖拽 horizontal vertical true
    onclick: () => unselectRect(echartsInstance) // 取消选中
  }
}
  1. 暴露方法: 根据 Echarts 实例及业务配置,框选图例对象
let RectInfo = {} // 矩形选中对象信息
let RectSelection = null // 矩形选中框

function selectLegend(echartsInstance, LegendConfig = {}) {
  if (!echartsInstance) return
  if (get(LegendConfig, 'closeSelection')) return // 关闭框选时,退出

  unselectRect(echartsInstance) // 移除旧选中框

  const zRender = echartsInstance.getZr() // 画布
  const rectOpts = getLegendSelection(echartsInstance) // 矩形

  RectInfo.type = MOUSE_TARGET_ENUM.LEGEND // 选中对象-图例
  RectSelection = createRectGraphic(rectOpts) // 创建图例选中框
  addGraphic(RectSelection, zRender) // 向画布添加图例选中框
}

框选 X 轴

  1. 设计方法: 根据 zRenderEcharts 实例,计算 X 轴选中框列表
// 获取 X 轴选中框列表(有多少个 X 轴,就含多少个选中框)
function getXAxisSelectionList(zRender, echartsInstance) {
  const echartsOption = echartsInstance.getOption() // 配置项

  if (!echartsOption) return []

  // X 轴列表
  const xAxisList = echartsOption.xAxis || []
  // 直角坐标系矩形范围
  const { x: gridX, y: gridY, width: gridW, height: gridH } = getGridRect(echartsInstance)

  return xAxisList.map((xAxis) => {
    const position = xAxis.position
    let y = 0

    if (position === 'bottom') { // 下
      y = gridY + gridH + xAxis.offset - 10 // 框上边界在轴线上侧 10px 的位置
    } else if (position === 'top') { // 上
      y = gridY - xAxis.offset - AXIS_OFFSET_DIFF + 10 // 框下边界在轴线下侧 10px 的位置
    }

    return {
      id: `${X_AXIS_UNIQUE}-selection`,
      x: toFixedPixel(gridX - AXIS_OFFSET_DIFF / 2),
      y: toFixedPixel(y),
      shape: {
        r: RECT_RADIUS,
        width: toFixedPixel(gridW, 0) + AXIS_OFFSET_DIFF, // 不固宽
        height: toFixedPixel(AXIS_OFFSET_DIFF, 0), // 固高
      },
      style: getSelectionStyle(),
      draggable: false, // 图形元素是否可以被拖拽 horizontal vertical true
      onclick: () => unselectRect(echartsInstance) // 取消选中
    }
  })
}
  1. 暴露方法: 根据 Echarts 实例及业务配置,框选 X 轴对象
let RectInfo = {} // 矩形选中对象信息
let RectSelection = null // 矩形选中框

export function selectXAxis(xAxisIndex, echartsInstance, AxisCfg = {}) {
  if (!echartsInstance) return
  if (get(AxisCfg, 'closeSelection')) return // 关闭框选时,退出

  unselectRect(echartsInstance) // 移除旧选中框

  const zRender = echartsInstance.getZr() // 画布
  const rectOpts = getXAxisSelectionList(zRender, echartsInstance)[xAxisIndex]

  RectInfo.type = MOUSE_TARGET_ENUM.XAXIS
  RectInfo.xAxisIndex = xAxisIndex
  RectSelection = createRectGraphic(rectOpts) // 创建 X 轴选中框
  addGraphic(RectSelection, zRender) // 向画布添加 X 轴选中框
}

框选 Y 轴

  1. 设计方法: 根据 zRenderEcharts 实例,计算 Y 轴选中框列表
// 获取 Y 轴选中框列表(有多少个 Y 轴,就含多少个选中框)
function getYAxisSelectionList(zRender, echartsInstance) {
  const echartsOption = echartsInstance.getOption() // 配置项

  if (!echartsOption) return []

  // Y 轴列表
  const yAxisList = echartsOption.yAxis || []
  // 直角坐标系矩形范围
  const { x: gridX, y: gridY, width: gridW, height: gridH } = getGridRect(echartsInstance)

  return yAxisList.map((yAxis) => {
    const position = yAxis.position
    let x = 0

    if (position === 'right') {
      x = gridX + gridW + yAxis.offset - 10 // 框左边界在轴线左侧 10px 的位置
    } else if (position === 'left') {
      x = gridX - yAxis.offset - AXIS_OFFSET_DIFF + 10 // 框右边界在轴线右侧 10px 的位置
    }

    return {
      id: `${Y_AXIS_UNIQUE}-selection`,
      x: toFixedPixel(x),
      y: toFixedPixel(gridY - AXIS_OFFSET_DIFF / 2),
      shape: {
        r: RECT_RADIUS,
        width: toFixedPixel(AXIS_OFFSET_DIFF, 0), // 固宽
        height: toFixedPixel(gridH, 0) + AXIS_OFFSET_DIFF, // 不固高
      },
      style: getSelectionStyle(),
      draggable: false, // 图形元素是否可以被拖拽 horizontal vertical true
      onclick: () => unselectRect(echartsInstance) // 取消选中
    }
  })
}
  1. 暴露方法: 根据 Echarts 实例及业务配置,框选 Y 轴对象
let RectInfo = {} // 矩形选中对象信息
let RectSelection = null // 矩形选中框

function selectYAxis(yAxisIndex, echartsInstance, AxisCfg = {}) {
  if (!echartsInstance) return
  if (get(AxisCfg, 'closeSelection')) return // 关闭框选时,退出

  unselectRect(echartsInstance)

  const zRender = echartsInstance.getZr() // 画布
  const rectOpts = getYAxisSelectionList(zRender, echartsInstance)[yAxisIndex]

  RectInfo.type = MOUSE_TARGET_ENUM.YAXIS
  RectInfo.yAxisIndex = yAxisIndex
  RectSelection = createRectGraphic(rectOpts) // 创建 Y 轴选中框
  addGraphic(RectSelection, zRender) // 向画布添加 X 轴选中框
}