对象框选
基本定义
本文对 框选对象 的定义是指:用户可以通过 点击 或 双击 等操作,用自定义矩形框,选中 Echarts 图表内的 图形元素。
众所周知,Echarts 本身并未暴露 直接框选对象 的能力。这也就是本文的价值所在:基于 Echarts 实现 创新性拓展。
本文重点介绍如何灵活框选 标题、坐标轴、图例 三大图形元素对象。
聪明的你,很容易就 预见性 地判断出,实现对象框选的主要难点在于:
- 灵活识别框选对象
- 动态更新框选尺寸
- 跟随内容更新方框
主要特点
对象框选将具备以下特点:
- 根据用户点击处位置,自动推算框选的对象
- 当框选对象内容变化,选中框自动改变尺寸
- 当框选对象位置变化,选中框自动跟随行动
- 当画布容器尺寸变化,选中框自动更新大小
- 选中框对象本身唯一,框选对象间存在互斥
- 选中对象具备优先级,标题>图例>X轴>Y轴
触发时机
- 单击对象区域
- 画布容器变化
- 对象配置更新
效果演示
👉👉👉 在线演示 👈👈👈
认识 ZRender
ZRender 是二维绘图引擎,它提供 Canvas、SVG、VML 等多种渲染方式。
它也是 ECharts 的渲染器。
我们可以通过 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()
}
}
通用工具
工具常量
- 公共常量: 矩形元素占位配置
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,
}
}
- 公共常量: 标题唯一标识前缀
// 画布唯一标识
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'
- 公共常量: 矩形元素圆角值
export const RECT_RADIUS = 2
- 公共常量: 框框对象枚举值
// 点击目标信息
export const MOUSE_TARGET_ENUM = {
BLANK: 0, // 空白
TITLE: 1, // 标题
LEGEND: 2, // 图例
XAXIS: 3, // x轴
YAXIS: 4, // y轴
VIEW: 5, // 绘图区域
}
- 公共常量: 多个坐标轴的固定偏移量
// 坐标轴 offset 差值(同时也是坐标轴选中框的固宽或固高)
export const AXIS_OFFSET_DIFF = 60
工具方法
- 工具方法: 根据 Echarts 实例 echartsInstance 移除旧选中框(即反选矩形选中框)
export function unselectRect(echartsInstance, reset = true) {
if (!echartsInstance) return
const zRender = echartsInstance.getZr() // 画布
zRender.remove(RectSelection) // 移除矩形选中框
if (reset) {
RectInfo = {}
RectSelection = null
}
}
- 工具方法: 根据 矩形配置 创建矩形图形元素 rect
export function createRectGraphic(RectOpts) {
if (!RectOpts) return
return new echarts.graphic.Rect(RectOpts)
}
- 工具方法: 向画布中添加图形元素 graphic
export function addGraphic(graphic, zRender) {
if (!graphic || !zRender) return
zRender.add(graphic)
}
- 工具方法: 选中框公共样式 graphic
export function getSelectionStyle() {
return {
fill: 'transparent',
stroke: '#E6A23C',
lineWidth: 1,
opacity: 0.6,
}
}
- 工具方法: 获取直角坐标系动态矩形 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
}
}
- 工具方法: 解决像素位置导致的线条太细时,颜色无法生效
import XEUtils from 'xe-utils'
export function toFixedPixel(pixel, diff = 0.5) {
return XEUtils.add(XEUtils.floor(pixel), diff) // XEUtils.floor 为向下取整
}
框选标题
- 设计方法: 根据 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
}
}
- 设计方法: 根据 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) // 取消选中
}
}
- 暴露方法: 根据 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) // 向画布添加标题选中框
}
框选图例
- 设计方法: 根据 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) // 取消选中
}
}
- 暴露方法: 根据 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 轴
- 设计方法: 根据 zRender 和 Echarts 实例,计算 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) // 取消选中
}
})
}
- 暴露方法: 根据 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 轴
- 设计方法: 根据 zRender 和 Echarts 实例,计算 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) // 取消选中
}
})
}
- 暴露方法: 根据 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 轴选中框
}