echarts 图表定制 02

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 轴已经被绿色的矩形框包裹了


    image.png

4.5 接下来的目标

  • 完成了自定义组件的基本框架,就可以在之上增加功能了。
  • 同样需要参考现有组件的定义,才能更好的制作自己的组件
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容