推荐一个基于solidjs的webcomponnet 图表组件, g2-render
区别于bizCharts,g2-plot 等传统组件,g2-render 在父组件写渲染逻辑,让你的项目避免成为💩山, 可json 序列化,支持报表配置
做了很多bi项目, 说一点自己的经验
一 echart vs g2 怎么选? 选 g2
- 封装组件的必要性和难易程度?
- 如果不封装,在单页应用里,势必要借助ref ,手动控制销毁, 代码重复还是小问题, 假如你的报表是在循环里使用, 那就很恶心了, 你得把每一个ref放到循环项的, 如此一来,项目只能是shi⛰️了,所以封装组件是必要的
- echart使用一个完整的对象配置,理解起来简单,而且容易封装,直接把option传递下去就行
- g2 使用链式api, 文档没echarts那么好, 而且v4版本的很多概念太学术了,ds,dv, 还有古怪的position=‘a*b’语法, 劝退了很多人, v5的api设计要简单很多,但是还不够完善
- 业务落地如何?
- echarts 接受数据的地方有两个, 新版的dataset.source,支持对象数据
- 旧版的series.data,不支持对象数据
{
// 按行的 key-value 形式(对象数组),这是个比较常见的格式。
source: [
{ product: 'Matcha Latte', count: 823, score: 95.8 },
{ product: 'Milk Tea', count: 235, score: 81.4 },
{ product: 'Cheese Cocoa', count: 1042, score: 91.2 },
{ product: 'Walnut Brownie', count: 988, score: 76.9 }
]
},
这个结构看起来没问题, 现在加点需求
场景一: 添加一条参考线,
- 假设数据是单独的一个数组:
那么直接新设置一条series.data即可 ? ❌ series.data不支持对象数组, 你比如使用dataset.source 预留一下这个新数组的位置 ,再配置sourcenIndex 来使用
- 数据是来自于整体的数据,使用filter过滤出来, 如果数据结构复杂, 处理起来更头疼,我看了vue-eachers的文档, 说什么echart有个开关,控制setOption是复杂还是添加,恐怖, 而且vue-echart 竟然还有自己的配置项, 我的理解不是简单的传递下我的配置项就行了吗, 你搞那么多配置,出了问题,我都不知道怎么排查,毕竟图表库我们普通前端也操作不了代码,全是配置
对于g2, 直接chart.line().data(modelData)即可,
场景二, 左侧折线图,联动右侧饼图, echart有现成demo
看起来是不是很高兴, 折线图的数据结构
source: [
['product', '2012', '2013', '2014', '2015', '2016', '2017'],
['Milk Tea', 56.5, 82.1, 88.7, 70.1, 53.4, 85.1],
['Matcha Latte', 51.1, 51.4, 55.1, 53.3, 73.8, 68.7],
['Cheese Cocoa', 40.1, 62.2, 69.5, 36.4, 45.2, 32.5],
['Walnut Brownie', 25.2, 37.1, 41.2, 18, 33.9, 49.1]
]
饼图的数据结构
encode: {
itemName: 'product',
value: '2012',
tooltip: '2012'
}
翻译成对象数组=>
[
{product: 'Milk Tea',2012:56.5 },
{product: 'Matcha Latte',2012:51.1 },
]
虽然都是对象数组, 却完全不兼容的,假设你的数据不是他这样的,第一行是x轴, 还是使用开头那样的对象数组, 那就gg吧, 谁吊大的来转换一下这两个数组,这个例子很容易误导人的思路,其实自己控制挺简单的
g2的折线图和饼图都是同一种对象结构,没有兼容问题
好了, 推荐g2,
二
举个例子, 大家一开始使用vue-chart,会自己把type:line以及其他配置封装进去,如果是配置型报表,还会封装数据请求的方式在里面, 或者g2-plot之类的组件库,直接使用line
但是后面发现自己封装的line组件,功能完全不够, 比如之前是一条,现在是多条(g2还好,就是多个字段,echarts的话,彻底gg吧, 一条和多条的数据结构不一样了)
或者接口数据结构变了, 或者是折线图右侧加饼图, 又得修改通用组件,慢慢的,你会发现通用组件越来越大,完全改不动了,
怎么解决呢?
把逻辑抽到外层,由父组件控制, 对于 echarts还好, 直接完整的配置,不做任何封装即可实现,但是配置太特么大了, 一层套一层,不看手册完全不知道自己要的那个属性在哪个层级下, 使用起来太难了
g2的链式api的优势就出来了, 五六行代码就可以出一个图,完全不用记忆属性的位置, 你只需要知道你要改的东西属于哪个通道即可, 很容易记忆
最后的问题就是: 组件是必须封装的, 但是又不想变成shit , 还想在父组件写渲染逻辑 但是,渲染函数中需要绑定一个div ref, 如果ref放父组件,在低代码这种配置系统里, 都是通过循环来生成图表, ref放循环项就很不方便不了,
所以, 既要支持报表配置, 又要在父组件控制渲染逻辑, 那就是把渲染函数系列化,之后作为prop传到子组件 ,子组件只控制创建和销毁, 不处理任何逻辑, 基于此,就可以直接拷贝g2官网里的demo来直接执行, 完全傻瓜式 最后, 能在react和vue项目里都使用就更好了, 这样你的配置就可以跨系统使用, 有web-component就更好了,
// 在vue中使用
<g2-render
:json="json"
:data.prop="data"
:scope.prop="scope"
:init-config.prop="{
syncViewPadding: true,
height: 300,
width: 200,
}"
debug="true"
></g2-render>
import { parser } from 'g2-render';
const render = ({ DataSet, chart, x, y, data }) => {
// ...
chart.render();
}
const json = parser.stringify({
render
});
所以 g2-render 诞生了
solid-js 编写的,源码如下
// index.ts
import { customElement } from "solid-element";
import MyComponent from "./MyComponent";
import parser from "./parser";
customElement(
"g2-render",
{ data: undefined, scope: {}, json: "", "init-config": {}, debug: false },
MyComponent
);
export { parser };
// MyComponent.tsx
import { Chart } from "@antv/g2";
import { InitCfg } from "@antv/g2/lib/geometry/base";
import { ChartCfg } from "@antv/g2/lib/interface";
import { Component, createEffect, onCleanup, onMount } from "solid-js";
import parser from "./parser";
export type Props = {
data?: any[];
scope: Record<string, any>;
json: string;
"init-config"?: Omit<ChartCfg, "container">;
debug?: boolean;
};
const MyComponent: Component<Props> = (props) => {
let div: HTMLDivElement;
let chart: Chart;
onMount(() => {
chart = new Chart({
container: div,
...props["init-config"],
});
onCleanup(() => chart.destroy());
});
/**
* {
...props.scope,
chart,
data: props.data ? props.data : [],
}
*/
createEffect(() => {
props.debug && console.log("props", props);
chart?.clear();
const scope = {
...props.scope,
chart,
data: props.data ? props.data : [],
};
if (!props.json) {
return;
}
try {
const parsed = parser.parse(props.json);
if (!parsed) {
console.error(
`[g2-render]: props.json参数格式不对, parse之后是 ${typeof parsed},期待parse成 object`
);
}
parsed.render(scope);
} catch (err) {
console.error(err);
}
});
return <div ref={div!}> {/*...*/} </div>;
};
export default MyComponent;
// parser.ts
interface IObject {
[name: string]: any;
}
const parser = {
FUNC_PREFIX: "__xfunc__",
REG_PREFIX: "__xreg__",
isArrOrObj: function (obj: any) {
return this.isArr(obj) || this.isObj(obj);
},
isArr: function (obj: any) {
return !!obj && Object.prototype.toString.call(obj) === "[object Array]";
},
isObj: function (obj: any) {
return !!obj && Object.prototype.toString.call(obj) === "[object Object]";
},
isRegExp: function (obj: any) {
return !!obj && Object.prototype.toString.call(obj) === "[object RegExp]";
},
isFunc: function (obj: any) {
return typeof obj === "function";
},
funcParse: function (obj: IObject | any[]) {
let result: any;
if (this.isArr(obj)) {
result = [];
result = obj.map((v: any) => {
/** 如果是函数类型, 则转换为字符串 */
if (this.isFunc(v)) {
return `${this.FUNC_PREFIX}${v}`;
}
/** 如果类型为正则 */
if (this.isRegExp(v)) {
return `${this.REG_PREFIX}${v}`;
}
/** 如果是数组或者对象 */
if (this.isArrOrObj(v)) {
return this.funcParse(v);
}
/** 基本类型 */
return v;
});
}
if (this.isObj(obj)) {
result = {};
for (let key in obj) {
const v = (obj as IObject)[key];
if (this.isFunc(v)) {
result[key] = `${this.FUNC_PREFIX}${v}`;
} else if (this.isRegExp(v)) {
/** 如果类型为正则 */
result[key] = `${this.REG_PREFIX}${v}`;
} else if (this.isArrOrObj(v)) {
result[key] = this.funcParse(v);
} else result[key] = v;
}
}
return result;
},
parse: function (jsonStr: string, error?: (err: Error | unknown) => {}) {
try {
return JSON.parse(jsonStr, (key, value) => {
if (value && typeof value === "string") {
const _value =
value.indexOf(this.FUNC_PREFIX) > -1
? new Function(`return ${value.replace(this.FUNC_PREFIX, "")}`)()
: value.indexOf(this.REG_PREFIX) > -1
? new Function(`return ${value.replace(this.REG_PREFIX, "")}`)()
: value;
return _value;
}
return value;
});
} catch (err) {
error && error(err);
}
},
stringify: function (
obj: any,
replacer?: (key: string, value: any) => any,
space?: number | string,
error?: (err: Error | unknown) => {}
) {
try {
let _obj = obj;
if (this.isRegExp(obj)) {
/** 如果类型为正则 */
_obj = `${this.REG_PREFIX}${obj}`;
}
if (this.isFunc(obj)) {
_obj = `${this.FUNC_PREFIX}${obj}`;
}
if (this.isArrOrObj(obj)) {
_obj = this.funcParse(obj);
}
return JSON.stringify(_obj, replacer, space);
} catch (err) {
error && error(err);
return "";
}
},
fastStringify: function (
obj: any,
space?: number | string,
error?: (err: Error | unknown) => {}
) {
try {
return JSON.stringify(
obj,
(k, v) => {
/** 如果类型为函数 */
if (this.isFunc(v)) {
return `${this.FUNC_PREFIX}${v}`;
}
/** 如果类型为正则 */
if (this.isRegExp(v)) {
return `${this.REG_PREFIX}${v}`;
}
return v;
},
space
);
} catch (err) {
error && error(err);
return "";
}
},
nativeStringify: JSON.stringify,
nativeParse: JSON.parse,
};
export default parser;