3.5 ECharts.setOption 基本逻辑
- /src/core/echart.ts 文件中的 ECharts 对象包含 setOption 函数,其主要功能是设置一个 echart 实例的 option,并实现数据更新。基本上 echarts 的图表数据更新都是通过这个函数实现的
- 这个函数有几个重构方法。其中参数的具体说明最好参考官方文档 setOption
- 基本上参数使用来处理 option 的合并和懒加载的。
- 具体的详细代码逻辑需要看源代码,并补全注释,同样清理 DEV 相关的调试代码
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
// 禁止在主进程设置
if (this[IN_MAIN_PROCESS_KEY]) {
return;
}
// 当前的实例已经删除,不再设置配置
if (this._disposed) {
disposedWarning(this.id);
return;
}
// 对配置项的合并进行处理
let silent;
let replaceMerge;
let transitionOpt: SetOptionTransitionOpt;
if (isObject(notMerge)) {
lazyUpdate = notMerge.lazyUpdate;
silent = notMerge.silent;
replaceMerge = notMerge.replaceMerge;
transitionOpt = notMerge.transition;
notMerge = notMerge.notMerge;
}
// 设定主进程的标记,避免重复设置
this[IN_MAIN_PROCESS_KEY] = true;
// 记录当前的 model,这里 _model 实际就是 GlobalModel 记录了 option的大部分信息。他的另一个名称是 ecModel
if (!this._model || notMerge) {
const optionManager = new OptionManager(this._api);
const theme = this._theme;
const ecModel = this._model = new GlobalModel();
ecModel.scheduler = this._scheduler;
ecModel.ssr = this._ssr;
ecModel.init(null, null, null, theme, this._locale, optionManager);
}
// 将实际的 option 添加到 _model之中,这里仅仅是保存 option,方便之后的组件使用
this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
// 建立更新的参数
const updateParams = {
seriesTransition: transitionOpt,
optionChanged: true
} as UpdateLifecycleParams;
// 判定是否是懒更新
if (lazyUpdate) {
// 处理懒更新参数,跳过
this[PENDING_UPDATE] = {
silent: silent,
updateParams: updateParams
};
this[IN_MAIN_PROCESS_KEY] = false;
// `setOption(option, {lazyMode: true})` may be called when zrender has been slept.
// It should wake it up to make sure zrender start to render at the next frame.
this.getZr().wakeUp();
}
else {
// 处理实时更新,会进一步调用渲染函数
try {
// 用于处理渲染调度,跳过
prepare(this);
// 实际的更新函数,其中会调用 render 方法
updateMethods.update.call(this, null, updateParams);
}
catch (e) {
// 出现错误,退出主进程
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
throw e;
}
// 处理非服务端渲染,保证 zrender 同步刷新
// Ensure zr refresh sychronously, and then pixel in canvas can be
// fetched after `setOption`.
if (!this._ssr) {
// not use flush when using ssr mode.
this._zr.flush();
}
// 退出主进程
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
// 刷新action,跳过
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
}
3.6 ECharts.internalField.updateMethods.update 函数逻辑
- 这个函数主要是用来处理 echart 实例更新的。
- 首先看参数
- this 是当前实例 ECharts
- payload 是事件的参数 Payload,第一次渲染是null,之后出现事件的时候会填充。
- updateParams 是传入的更新参数 UpdateLifecycleParams
- 之后看其内部逻辑,通过源代码查看
update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void {
// 从 this 获取基本的数据模型ecModel、接口api、渲染器zr、坐标系统管理coordSysMgr、调度器 _scheduler
const ecModel = this._model;
const api = this._api;
const zr = this._zr;
const coordSysMgr = this._coordSysMgr;
const scheduler = this._scheduler;
// 处理异常,数据模型不存在,则退出
// update before setOption
if (!ecModel) {
return;
}
// 设置数据模型的事件负载
ecModel.setUpdatePayload(payload);
// 处理调度器
scheduler.restoreData(ecModel, payload);
scheduler.performSeriesTasks(ecModel);
// 建立坐标系统管理器
// Create new coordinate system each update
// In LineView may save the old coordinate system and use it to get the original point.
coordSysMgr.create(ecModel, api);
// 处理调度器的数据处理
scheduler.performDataProcessorTasks(ecModel, payload);
// 处理数据的stream
// Current stream render is not supported in data process. So we can update
// stream modes after data processing, where the filtered data is used to
// determine whether to use progressive rendering.
updateStreamModes(this, ecModel);
// 更新坐标系统
// We update stream modes before coordinate system updated, then the modes info
// can be fetched when coord sys updating (consider the barGrid extent fix). But
// the drawback is the full coord info can not be fetched. Fortunately this full
// coord is not required in stream mode updater currently.
coordSysMgr.update(ecModel, api);
// 清空颜色盘
clearColorPalette(ecModel);
// 调度器处理可视化任务
scheduler.performVisualTasks(ecModel, payload);
// 执行渲染逻辑,关键
render(this, ecModel, api, payload, updateParams);
// 设置背景色
// Set background
const backgroundColor = ecModel.get('backgroundColor') || 'transparent';
// 设置主题模式
const darkMode = ecModel.get('darkMode');
// 设置渲染器背景色
zr.setBackgroundColor(backgroundColor);
// 强制渲染器暗色模式
// Force set dark mode.
if (darkMode != null && darkMode !== 'auto') {
zr.setDarkMode(darkMode);
}
// 触发 afterupdate 事件
lifecycle.trigger('afterupdate', ecModel, api);
},
- 这个 update 函数最关键的部分就是 render 函数的调用。其中调用了全部的关键参数。目前最初 option 相关的数据都存储于 ecModel 之中了。
3.7 查看 ECharts.internalField.render 函数
- 这个函数组合了多个函数的功能,依次实现了组件渲染,和视图渲染。
- 具体查看其中的代码
render = (
ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
updateParams: UpdateLifecycleParams
) => {
// 处理 zlevel 属性,前后的覆盖关系
allocateZlevels(ecModel);
// 渲染组件
renderComponents(ecIns, ecModel, api, payload, updateParams);
// 渲染视图,标记全部视图为不活动
each(ecIns._chartsViews, function (chart: ChartView) {
chart.__alive = false;
});
// 对视图系列进行渲染
renderSeries(ecIns, ecModel, api, payload, updateParams);
// 删除没有渲染的视图
// Remove groups of unrendered charts
each(ecIns._chartsViews, function (chart: ChartView) {
if (!chart.__alive) {
chart.remove(ecModel, api);
}
});
};
- 进一步的渲染方法就是 renderComponents 和 renderSeries,其中 renderComponent 比较简单
- 其中多了一个参数是 dirtyList 使处理脏的组件列表。
renderComponents = (
ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[]
) => {
// 遍历实例中注册的组件和脏列表之中的组件
each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) {
// 获得组件的模型
const componentModel = componentView.__model;
// 清理组件状态
clearStates(componentModel, componentView);
// 调用组件视图的渲染函数,关键
componentView.render(componentModel, ecModel, api, payload);
// 更新z高度
updateZ(componentModel, componentView);
// 更新状态
updateStates(componentModel, componentView);
});
};
- 至此就清楚关于组件渲染的流程了。如果需要定制组件,那么就要实现 ComponentView 的 render 函数就可以了。
- 对于 ChartView 主要是针对 Series 的渲染。所以如果是自定义坐标轴相关的组件,只用 ComponentView 就够了。
4. 自定义 ComponentView
4.1 目标
- 仿照 dataZoom 组件定义一个的x坐标轴的组件
- 使用矩形绘制整个坐标轴范围内容
- 对于现有 x 坐标轴仅仅隐藏。
- 新的坐标轴可以跟随 x 轴的移动更新。
4.2 渲染需求
- 坐标轴组件需要定位在原来的x轴坐标的位置
- 使用矩形包裹全部 x 轴
- splitline使用直线绘制
- tickLabel在中间显示
- 隐藏现有 x 轴
4.3 组件创建
4.3.1 MyAxisModel
- MyAxisModel 是组件的数据模型,继承自ComponentModel
- 模型之中可以附加 MyAxisOption,作为附加数据选项,方便在之后的配置项中进行设置
- 基本代码如下
import * as echarts from "echarts/core";
// 数据模型选项的接口,可以在之后的配置项中和其他选项合并
// 实际应当继承 ComponentOption,但是此类并没有对外开放
export interface MyAxisOption {
// 配置项的名称,自定义的组件这个名称最好相同。否则可能找不到组件
// 在 echarts 的配置项中需要使用此名称来获得对应组件数据模型和视图
mainType?: "myAxis";
}
// 实际的数据模型
class MyAxisModel extends echarts.ComponentModel<MyAxisOption> {
// 数据模型的类型,自定义组件此名称最好相同
static type = "myAxis";
type = MyAxisModel.type;
}
export default MyAxisModel;
- 上面的数据模型并没有添加任何的选项,但是作为与 echarts 的配置项的接口,这个数据模型是必须的。否则找不到对应的视图。
4.3.2 MyAxisView
- MyAxisView 需要继承自 ComponentView
- 实现自定义可以主要通过重载 render 函数实现
- 内部的具体逻辑参考代码即可
import * as echarts from "echarts/core";
import MyAxisModel from "./myAxisModel";
// 继承自 ComponentView
class MyAxisView extends echarts.ComponentView {
static type = "myAxis";
type = MyAxisView.type;
// 重载渲染函数,
// _model 就是组件的数据模型
// ecModel 就是 GlobalModel, 通过这个模型可以获得其他组件信息
// _api 是获取 dom 相关信息的接口
// _payload 是事件相关数据
render(_model: MyAxisModel, ecModel: any, _api: any, _payload: any) {
// ComponentView 类的图形容器组,全部的 zrender 绘制的内容都是要添加到这个 group 之中
const group = this.group;
// 获得第一个x坐标轴
const xAxisModel = ecModel.getComponent("xAxis", 0);
// 获得x坐标轴的坐标系统
const axisCoordSysModel = xAxisModel.getCoordSysModel();
const coorSys = axisCoordSysModel.coordinateSystem;
// 获得x坐标轴的显示范围
const axisRect = coorSys.getRect();
// 绘制包裹x坐标轴的矩形,这里使用的是利用 zrender 封装的图形接口
const rect = new echarts.graphic.Rect({
shape: {
x: axisRect.x,
y: axisRect.y + axisRect.height,
width: axisRect.width,
height: 30,
},
style: {
fill: "none",
stroke: "#0F0",
},
});
// 添加矩形到 group,完成渲染
group.add(rect);
}
}
export default MyAxisView;
- 自定义组件基本上可以获得 echarts 其他组件的全部信息
- 上面的代码中就是获得了第一个 x 轴的坐标信息,从而绘制了包裹 x 轴的绿色矩形框
- 参考 dataZoom 组件可以获取更多的 echarts 图表的其他组件的信息
- 其中获取其他组件的方法主要是以下的函数,获取的都是组件数据模型:
- GlobalModel.getComponent,通常在 view 之中,通过 ecModel 调用
- ComponentModel.getReferringComponents,通常在 model 之中调用
4.3.3 install
- 设置完成 view 和 model,就需要添加一个 install 文件
- 这个文件主要是用来注册组件的视图和模型的,在实际使用中引入的组件文件也是这个install文件。
- 之后 echarts 会使用 use 函数来调用其中的 intall 函数完成注册
- 没有注册的组件即使出现在配置项中,也是无法识别的。
import MyAxisModel from "./myAxisModel";
import MyAxisView from "./myAxisView";
// intall 函数实际就是注册函数
// 当 echart 使用 use 函数的时候会调用 install 函数,完成注册
// 只有注册的组件才能通过配置项配置,并渲染
export default function install(registers: any) {
// 注册组件模型
registers.registerComponentModel(MyAxisModel);
// 注册组件视图
registers.registerComponentView(MyAxisView);
}
4.4 使用组件
- 具体使用组件实际就与echarts的其他组件的使用方式类似了。
- 首先引入组件的install函数,通过 use 注册完成
- 之后在配置项中写上组件的type对应的名称,如果有配置,在对应的配置中添加即可。
- 修改之后的 echarts 示例如下
import "../style.css";
import * as echarts from "echarts/core";
import {
BarChart,
// 系列类型的定义后缀都为 SeriesOption
BarSeriesOption,
} from "echarts/charts";
import {
TitleComponent,
// 组件类型的定义后缀都为 ComponentOption
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
// 数据集组件
DatasetComponent,
DatasetComponentOption,
} from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
// 自定义组件
import MyAxis from "../myAxis/install";
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type ECOption = echarts.ComposeOption<
| BarSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
>;
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
BarChart,
CanvasRenderer,
MyAxis, // 注册自定义组件
]);
// 绘制图表
const option: ECOption = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 20],
},
],
// 添加自定义组件配置项
myAxis: {},
};
// 基于准备好的dom,初始化echarts实例
const elem = document.getElementById("main");
if (elem) {
const myChart = echarts.init(elem);
// 绘制图表
myChart.setOption(option);
}
-
最终效果如下图,可以看到 x 轴已经被绿色的矩形框包裹了
4.5 接下来的目标
- 完成了自定义组件的基本框架,就可以在之上增加功能了。
- 同样需要参考现有组件的定义,才能更好的制作自己的组件