echarts是常用图表,经常需要点进去官网改示例代码,为了方便配置echarts图表样式,教你快速开发个小工具。
1.爬取echarts官网配置项手册
- axios请求获取echarts基础配置项,链接
https://echarts.apache.org/zh/documents/option-parts/option.js?2191a12b13
js
//getBaseOptions.js
import axios from 'axios';
import fs from 'node:fs';
const optionsUrl = `https://echarts.apache.org/zh/documents/option-parts/option.js?2191a12b13`;
function getEchartsDoc() {
axios.get(optionsUrl).then(async ({ data }) => {
fs.writeFileSync('./options.js', data.toString().replace('window.__EC_DOC_option =', 'export default '));
console.log('options ok');
});
}
getEchartsDoc();
注意:echarts的option参数挂载在window.__EC_DOC_option
,可以替换为export default
转换为esmodule
2.爬取全部配置项详细设置
- 用option的key属性值,跟上面获取基础配置项一样,获取详细配置,链接:
https://echarts.apache.org/zh/documents/option-parts/option.${k}.js?2191a12b13
,需要替换${k}
为对应的key
- 另外,详细配置项挂载在window上的参数也要对应调整,没有
-
属性名如title对应window.__EC_DOC_option_title
,有-
属性名如dataZoom-inside
则要将-
替换成_
,对应window.__EC_DOC_option_dataZoom_inside
js
//getEchartsOptions.js
import axios from 'axios';
import fs from 'node:fs';
import options from './options.js';
const failItems = [];
async function getItem(k) {
//替换window挂载参数
const n = k.replace('-', '_');
try {
const data = await axios
.get(`https://echarts.apache.org/zh/documents/option-parts/option.${k}.js?2191a12b13`, { timeout: 10000 })
.then(({ data }) => data.toString().replace(`window.__EC_DOC_option_${n} = `, 'export default '));
fs.writeFileSync(`./echarts/${k}.js`, data);
console.log(k + ' ok');
} catch (error) {
//收集获取错误的配置项
failItems.push(k);
console.log(k + ' fail');
}
}
//失败获取的详细配置再获取一遍
async function getFailItems() {
for (let i = 0; i < failItems.length; i++) {
await getItem(failItems[i]);
}
}
//遍历key获取详细配置项
async function getEchartItems() {
for (let k in options) {
await getItem(k);
}
console.log(failItems);
getFailItems();
}
getEchartItems();
注意:因为echarts访问服务端有限制或者网络比较慢,可能会出现请求失败的情况,那么就需要记录错误的请求,重新获取一遍。
3.将爬取的配置项转换成表单配置
echarts的详细配置项组成格式大概是这样
ts
type EchartsDocType = {
[key: string]: {
desc: string;
uiControl?: { default?: string | number | boolean } & (
| { type: 'enum'; options: string }
| { type: 'color' | 'boolean' }
| { type: 'number'; min?: number; max?: number; step: number }
);
};
};
配置项转换为表单对应的类型
ts
const compMap = {
number: InputNumber,//数值输入
text: Input,//文本输入
color: EchartsColor,//echarts颜色选择器(渐变或纯色)
select: Select,//下拉框
boolean: Checkbox,//是否勾选
symbol: SymbolPicker,//图形类型选择(形状或图片)
adcode: AdcodeSelect//行政区选择,adcode
};
读取echarts doc详细配置,转换成表单类型
js
//getEchartsForm.js
import fs from 'node:fs';
import options from './options.js';
import cheerio from 'cheerio';
//转换为form配置项
async function transformEchartsItem(name, cfg) {
let data;
try {
data = await import(`./echarts/${name}.js`).then((res) => res.default);
} catch (error) {
if (cfg.uiControl) {
data = { [name]: cfg };
} else {
return;
}
}
const list = [];
let rangeSet=[];
for (let k in data) {
if (k.indexOf('.<') > 1) continue;
const item = data[k];
const c = item.uiControl;//输入类型
const key = k.substring(k.lastIndexOf('.') + 1);//显示文本
const set = { inputType: 'text', label: key };//默认是文本输入
if (key === 'symbol') {//形状选择
set.inputType = 'symbol';
} else if (key === 'map') {//adcode行政区选择
set.inputType = 'adcode';
} else if (key.indexOf('Color') >= 0) {//颜色选择
set.inputType = 'color';
} else if (['inRange', 'outOfRange'].includes(k)) {
//特殊处理,visualMap视觉映射的颜色范围
rangeSet.push({ c, k, key, item });
continue;
}else if (!c && item.desc.indexOf(':</p>\n<ul>') > -1) {//部分输入类型是可选的,解析desc字段
//...
} else if (c) {//有uiControl设置
if (c.type === 'enum') {//枚举类型=>下拉框
set.inputType = 'select';
set.options = c.options.split(',');
set.options.default = c.default;
} else if (c.type === 'number') {//数值输入
set.inputType = 'number';
if (c.min !== undefined) {
set.min = Number(c.min);
}
if (c.max !== undefined) {
set.max = Number(c.max);
}
if (c.step !== undefined) {
set.step = Number(c.step);
}
} else if (c.type === 'color') {//颜色选择
set.inputType = 'color';
} else if (c.type === 'boolean') {//勾选框
set.inputType = 'boolean';
}
//默认值
if (c.default !== 'null' && c.default !== 'undefined' && c.default !== undefined) {
set.default = c.type === 'number' ? Number(c.default) : c.type === 'boolean' ? Boolean(c.default) : c.default;
}
}
list.push({
id: name + '.' + k,
code: k,
...set
});
}
let formList = getChildForm(list, name);
//特殊处理,visualMap.inRange,outOfRange视觉映射颜色范围
if (rangeSet.length) {
rangeSet.forEach((s) => {
formList.push({
inputType: 'children',
title: s.key,
code: s.k,
id: name + '.' + s.k,
config: [
{
id: name + '.' + s.k + '.color',
code: s.k + '.color',
inputType: 'text',
label: 'color'
}
]
});
});
}
fs.writeFileSync(`../src/components/RightPanel/echartsForm/${name}.ts`, 'export default ' + JSON.stringify(formList));
console.log(name + ' ok');
}
async function getFroms() {
for (let k in options) {
await transformEchartsItem(k, options[k]);
}
}
getFroms();
- 部分输入类型是可选的,但是没有uiControl,需要解析desc字段
js
if (!c && item.desc.indexOf(':</p>\n<ul>') > -1) {
const $ = cheerio.load(item.desc);//将字符串转为dom,类似jquery操作
set.inputType = 'select';
let ops = [];
let df = '';
//解析选项
$('ul>li').each((i, a) => {
const code = $(a).find('code.codespan')[0];
const b = $(code).text().replace(/'/g, '');
ops.push(b);
const t = $(a).text();
if (t.indexOf('默认') > -1) {//默认选项
df = b;
}
});
set.options = ops;
if (df) {
set.options.default = df;
}
}
因为是扁平属性,很多重名的属性平铺在外层会很乱,不知是哪个里面的,则要对属性进行分类,将同一父级属性的输入配置放在一起
js
function getChildForm(formList, name) {
let list = [];
const listMap = {};
//遍历获取子级属性
formList.forEach((item) => {
const c = item.nextCode || item.code;
if (c.indexOf('.') > 1) {
const k = c.substring(0, c.indexOf('.'));
const l = {
...item,
nextCode: c.substring(c.indexOf('.') + 1)
};
if (listMap[k]) {
listMap[k].push(l);
} else {
listMap[k] = [l];
}
} else {
list.push(item);
}
});
if (Object.keys(listMap).length) {
//去除父级输入,避免重复
list = list.filter((a) => !listMap[a.nextCode || a.code]);
//添加子级属性,有数组也有对象
for (const k in listMap) {
list.push({
inputType: ['data', 'indicator'].includes(k) ? 'arr' : 'children',
title: k,
code: k,
id: name + '.' + k,
config: getChildForm(listMap[k], name + '.' + k)
});
}
}
return list;
}
4.动态添加option属性
- 拖拽添加属性,然后可点击X删除
- 其中,普通配置不能重复,系列配置可以是多个,禁止拖入错误的位置
tsx
import { type DragEvent } from 'react';
import styles from './ChartList.module.scss';
import optionsKeys from './optionsKeys';
import { CloseOutlined } from '@ant-design/icons';
export const ChartList = (props: {
chartOptions: string[];
chartSeries: string[];
onChange: (type: string, v: string[]) => void;
}) => {
let dragItem: HTMLElement;
const onDragStart = (ev: DragEvent) => {
dragItem = ev.target as HTMLElement;
};
const onDragOver = (ev: DragEvent) => {
ev.preventDefault();
};
//拖入框内
const onDragItem = (ev: DragEvent, type: string) => {
ev.preventDefault();
const item = dragItem.dataset.item as string;
if (type === 'series') {
//禁止错误类型拖入
if (item.indexOf('series') === -1) return;
const v = props.chartSeries;
v.push(item);
props.onChange(type, [...v]);
} else {
//禁止错误类型拖入,并且不可重复
if (item.indexOf('series') > -1 || props.chartOptions.includes(item)) return;
const v = props.chartOptions;
v.push(item);
props.onChange(type, [...v]);
}
};
//删除项
const onDelItem = (type: string, idx: number) => {
if (type === 'series') {
const v = props.chartSeries;
v.splice(idx, 1);
props.onChange(type, [...v]);
} else {
const v = props.chartOptions;
v.splice(idx, 1);
props.onChange(type, [...v]);
}
};
return (
<div className={styles.chartList}>
{/*可选配置*/}
<div className={styles.chartOptions}>
{optionsKeys.map((it) => (
<span draggable data-item={it} onDragStart={onDragStart} className={styles.optionItem} key={it}>
{it}
</span>
))}
</div>
{/*基础配置拖入框*/}
<div className={styles.chartSelect}>
<div className={styles.title}>配置项</div>
<div className={styles.dragBox} onDrop={(ev) => onDragItem(ev, 'options')} onDragOver={onDragOver}>
{props.chartOptions.map((it, idx) => (
<span data-item={it} className={styles.optionItem} key={'op-' + it + idx}>
{it} <CloseOutlined onClick={() => onDelItem('options', idx)} className={styles.deleteIcon} />
</span>
))}
</div>
{/*系列拖入框*/}
<div className={styles.title}>系列</div>
<div className={styles.dragBox} onDrop={(ev) => onDragItem(ev, 'series')} onDragOver={onDragOver}>
{props.chartSeries.map((it, idx) => (
<span data-item={it} className={styles.optionItem} key={'s-' + it + idx}>
{it} <CloseOutlined onClick={() => onDelItem('series', idx)} className={styles.deleteIcon} />
</span>
))}
</div>
</div>
</div>
);
};
5.动态生成表单配置面板
动态表单
输入项FormItem,输入类型根据上面的转换类型compMap
的动态输入组件
tsx
type FormInputProps1 = {
code: string;
label?: string;
} & (NumInputProps1 | SelectProps1 | TextProps1 | CheckBoxProps1);
export const FormItem = memo(
(
props: FormInputProps1 & {
onChange: (code: string, value: any) => void;
}
) => {
//临时值
const [propVal, setPropVal] = useState(props.value);
//动态输入组件
const InputEl: any | FC = compMap[props.inputType];
const changeValue = (ev: any) => {
const code = props.code;
let v;
//文本输入类型
if (props.inputType === 'text') {
v = ev.target.value;
//勾选框
} else if (props.inputType === 'boolean') {
v = ev.target.checked;
} else {
v = ev;
}
setPropVal(v);
props.onChange(code, v);
};
//监听值更新
useEffect(() => {
setPropVal(props.value);
return () => {};
}, [props.value]);
//不同输入类型的属性
let attrs = {};
if (props.inputType === 'number') {
//数值输入
attrs = { value: propVal, min: props.min, step: props.step, max: props.max };
} else if (props.inputType === 'boolean') {
//勾选框
attrs = { checked: propVal === true };
} else if (props.inputType === 'select') {
//下拉框
attrs = { value: propVal, options: props.options.map((it) => ({ label: it, value: it })) };
} else {
attrs = { value: propVal };
}
return (
<div className={styles.formItem}>
{/* 显示文本 */}
{props.label ? <span className={styles.formLabel}>{props.label}</span> : ''}
<span className={styles.formInput}>
<InputEl {...attrs} onChange={changeValue} size="small" />
</span>
</div>
);
}
);
- 表单配置面板FormList,表单配置面板嵌套子级FormList和FormArr数组表单面板
tsx
export const FormList = memo(
(props: {
title?: string; //标题
isNextCode?: boolean; //是否子级
config: Array<FormItemConfig>; //表单配置
value: FormItemValue; //值的对象
onChange: (val: FormItemValue) => void;
parent?: string; //父级code
isArr?: boolean; //是否数组
}) => {
const initVal = props.isArr ? [] : {};
const [propVal, setPropVal] = useState<FormItemValue>(props.value || initVal);
const [isShow, setIsShow] = useState(false);
//监听值更新
useEffect(() => {
setPropVal(props.value);
return () => {};
}, [props.value]);
//单个输入项更新,采用扁平属性设置`setFlatObj`
const onChangeItem = (code: string, value: any) => {
let v = propVal;
const parentPrefix = props.parent ? props.parent + '.' : '';
setFlatObj(v, parentPrefix + code, value);
//isArr时转换为数组
if (props.isArr && !Array.isArray(v)) {
const keys = Object.keys(v).sort((a, b) => Number(a) - Number(b));
const arr: FormItemValue = [];
keys.forEach((it) => {
arr[it] = v[it];
});
v = arr;
}
setPropVal(v);
props.onChange(v);
};
//展开配置和收起配置
const changeShow = () => {
setIsShow(!isShow);
};
//数组表单面板更新
const onChangeArr = (code: string, value: FormItemValue) => {
const v = propVal;
setFlatObj(v, code, value);
props.onChange(v);
};
//获取单个输入项
const getFormItem = (item: FormInputProps, key: string) => {
//获取扁平属性的值
let val = getFlatObj(propVal, key);
if (val === undefined) val = item.default;
return <FormItem {...item} value={val} key={item.id} code={item.code} onChange={onChangeItem} />;
};
const list: any[] = [];
const p = props.parent ? props.parent + '.' : '';
for (let i = 0; i < props.config.length; i++) {
const item = props.config[i];
const key = p + item.code;
if (item.inputType === 'arr') {
//数组表单面板
const val = getFlatObj(propVal, key);
list.push(
<FormArr
title={item.title}
value={val}
key={item.id}
code={key}
config={item.config}
onChange={onChangeArr}
></FormArr>
);
} else if (item.inputType === 'children') {
//子级表单面板
list.push(
<FormList
isNextCode={true}
title={item.title}
value={propVal}
key={item.id}
config={item.config as FormItemConfig[]}
onChange={props.onChange}
parent={props.parent}
></FormList>
);
} else if (item.inputType === 'multi') {
//多个输入并列
const cfg = item.config as FormInputProps[];
const items: any[] = [];
for (let j = 0; j < cfg.length; j++) {
const it = cfg[j];
const kk = p + it.code;
items.push(getFormItem(it, kk));
}
list.push(<div key={item.id}>{items}</div>);
} else {
//单个输入项
list.push(getFormItem(item, key));
}
}
return (
<div className={styles.formList}>
{props.title ? (
<div className={styles.formTitle}>
<span onClick={changeShow}>
{isShow ? <CaretDownOutlined /> : <CaretRightOutlined />} {props.title}
</span>
</div>
) : (
''
)}
{isShow || !props.title ? <div className={props.isNextCode ? styles.formChild : ''}>{list}</div> : ''}
</div>
);
}
);
采用扁平属性的好处是,可以随意放置表单输入项的位置,只要保持扁平属性对得上就可以。如果需要定制化开发,也可以根据需求调整表单面板的排版。
ts
/**
* 给obj的扁平属性设置新值
* @param obj 对象或数组
* @param str 扁平属性,如{a:{b:[{c:1}]}}=>'a.b[0].c'
* @param value 新值
* @returns
*/
export function setFlatObj(
obj: {
[n: string | number]: any;
},
str: string,
value: any
) {
if (!obj) {
obj = {};
}
const attrs = str.replace(/[\[\]]+/g, '.').split('.');
let temp = obj;
for (let i = 0; i < attrs.length; i++) {
const n = attrs[i];
if (n) {
if (i === attrs.length - 1) {
temp[n] = value;
} else {
if (!temp[n]) {
//如果是空值则设置为空对象
temp[n] = {};
}
temp = temp[n];
}
}
}
return obj;
}
/**
* 获取扁平属性的值
* @param obj 对象或数组
* @param str 扁平属性,如{a:{b:[{c:1}]}}=>'a.b[0].c'
* @returns
*/
export function getFlatObj(
obj: {
[n: string | number]: any;
},
str: string
) {
if (!obj) {
obj = {};
}
let temp = obj;
const attrs = str.replace(/[\[\]]+/g, '.').split('.');
for (let i = 0; i < attrs.length; i++) {
const n = attrs[i];
if (n) {
if (i === attrs.length - 1) {
return temp[n];
} else {
if (temp[n] === undefined) {
return;
}
temp = temp[n];
}
}
}
}
- 数组表单配置,与FormList类似,但有数组的增删改查。不要遍历数组生成n个表单,只要一个表单就好,通过切换tab来切换当前对象。
tsx
export const FormArr = (props: {
title: string;//标题
config: Array<FormItemConfig1>;//表单配置
value: FormItemValue;//数组对象
code: string;//属性
onChange: (code: string, val: FormItemValue) => void;
}) => {
const [propVal, setPropVal] = useState(props.value || []);
const [isShow, setIsShow] = useState(false);
const [selectTab, setSelectTab] = useState(0);
//当前选中的数组元素
const [currentVal, setCurrentVal] = useState({});
const changeShow = () => {
setIsShow(!isShow);
};
const onChangeVal = (value: FormItemValue) => {
const v = propVal;
v[selectTab] = value;
setPropVal(v);
props.onChange(props.code, v);
};
const items: TabsItem[] = [];
const newconfig = useMemo(() => props.config.map((it) => ({ ...it, code: it.nextCode })), [props.config]);
//生成tabs
for (let i = 0; i < propVal.length; i++) {
const idx = i + 1 + '';
items.push({
value: i,
label: props.title + idx
});
}
//切换当前配置对象
const onChangeTab = (tab: string | number) => {
setSelectTab(tab as number);
setCurrentVal({ ...propVal[tab] });
};
//添加配置
const onAdd = () => {
const v = propVal;
v.push({});
setPropVal(v);
props.onChange(props.code, v);
onChangeTab(v.length - 1);
};
//删除数组的对象
const onDel = () => {
const i = selectTab;
const v = propVal;
v.splice(i, 1);
setPropVal(v);
props.onChange(props.code, v);
if (selectTab > v.length - 1) {
onChangeTab(v.length - 1);
}
};
//初始化默认当前选项
useEffect(() => {
setCurrentVal(propVal[selectTab]);
return () => {};
}, []);
return (
<div className={styles.formList}>
<div className={styles.formTitle}>
<span onClick={changeShow}>
{isShow ? <CaretDownOutlined /> : <CaretRightOutlined />} {props.title}
</span>
<span className={styles.formTitleRight}>
<PlusOutlined onClick={onAdd} /> <DeleteOutlined onClick={onDel} />
</span>
</div>
{isShow ? (
propVal.length ? (
<div className={styles.formArr}>
<ScrollTabs tabs={items} active={selectTab} onChange={onChangeTab} itemWidth={60}></ScrollTabs>
<FormList value={currentVal} config={newconfig} onChange={onChangeVal}></FormList>
</div>
) : (
<Empty />
)
) : (
''
)}
</div>
);
};
更多的定制化表单需求
- 改变对应值影响其他输入项的显示隐藏,其他输入项的可用与否,或者其他输入项值的改变
- 表单项可以是一对一,一对多,多对多的关联关系。
- 全局属性值,修改全局值整体改变,没有修改的默认属性值就会同步成全局属性值。
- 下拉框可选项等动态请求
- 表单校验
- 判断特殊的定制化输入组件 等等
之前搞低代码都是一套表单实现不同控制的逻辑,配个js就搞定很方便,组件复用,效率很高,真心不推荐一个个手写表单。
生成面板
- 动态获取表单配置,用import异步加载。
ts
interface configMap {
[n: string]: Array<FormItemConfig>;
}
interface FormConfig {
title: string;
config: Array<FormItemConfig>;
}
const configMap: configMap = {};
function getConfigJSON(name: string) {
return new Promise<Array<FormItemConfig>>((resolve) => {
//缓存配置
if (configMap[name]) resolve(configMap[name]);
else {
//获取echarts的表单配置
import(`./echartsForm/${name}.ts`)
.then(({ default: data }) => {
if (typeof data === 'object') {
configMap[name] = data as Array<FormItemConfig>;
} else {
configMap[name] = [{ inputType: 'text', code: name, label: name, id: name }];
}
resolve(configMap[name]);
})
.catch(() => {//如果没有配置就生成默认配置
configMap[name] = [{ inputType: 'text', code: name, label: name, id: name }];
resolve(configMap[name]);
});
}
});
}
- 获取拖入的配置属性的对应表单设置,生成面板
tsx
export const RightPanel = (props: {
chartOptions: string[]; //echarts的普通配置属性
chartSeries: string[]; //echarts的系列配置属性
optionsConfig: FormItemValue; //echarts的普通配置属性的值
seriesConfig: FormItemValue; //echarts的系列配置属性值
onChange: (ev: { type: string; v: FormItemValue }) => void;
}) => {
const [baseOpList, setBaseOpList] = useState<FormItemConfig[]>([]);
const [optionsList, setOptionsList] = useState<FormConfig[]>([]);
const [seriesList, setSeriesList] = useState<FormConfig[]>([]);
//更新获取表单配置面板
const updatePanel = async () => {
//基础配置
{
const list: FormConfig[] = [];
const baseList: FormItemConfig[] = [];
for (let i = 0; i < props.chartOptions.length; i++) {
const item = props.chartOptions[i];
const set = await getConfigJSON(item);
if (set.length === 1) {
baseList.push(set[0]);
} else {
list.push({ title: item, config: set,
code: item.indexOf('-') ? item.substring(0, item.indexOf('-')) : item } as FormChildConfig);
}
}
setBaseOpList(baseList);
setOptionsList(list);
}
//系列配置
{
const s = props.seriesConfig;
const list: FormConfig[] = [];
for (let i = 0; i < props.chartSeries.length; i++) {
const item = props.chartSeries[i];
const set = await getConfigJSON(item);
list.push({ title: item, config: set, code:item } as FormChildConfig);
if (s[i] === undefined) {
//不存在系列则添加系列
const type = item.substring(item.indexOf('-') + 1);
s[i] = { type, data: [], id: uuid() };
}
}
onChangeSeries(s);
setSeriesList(list);
}
};
const onChangeSeries = (v: FormItemValue) => {
props.onChange({ type: 'series', v });
};
const onChangeValue = (v: FormItemValue) => {
props.onChange({ type: 'options', v });
};
//监听配置属性,则更新面板
useEffect(() => {
updatePanel();
return () => {};
}, [props.chartOptions, props.chartSeries]);
return (
<div className="rightPanel">
{/* 基础配置面板 */}
{props.chartOptions.length === 0 && props.chartSeries.length === 0 ? (
<Empty description="请选择echarts配置"></Empty>
) : (
''
)}
<FormList config={baseOpList} value={props.optionsConfig} onChange={onChangeValue}></FormList>
{optionsList.map((it) => (
<FormList
title={it.title}
parent={it.code}
config={it.config}
value={props.optionsConfig}
key={it.title}
onChange={onChangeValue}
></FormList>
))}
{/* 系列配置面板 */}
{seriesList.map((it, i) => (
<FormList
title={i + 1 + '.' + it.title}
parent={i + ''}
config={it.config}
isArr={true}
value={props.seriesConfig}
key={i + it.title}
onChange={onChangeSeries}
></FormList>
))}
</div>
);
};
6.生成图表
- echarts图表的options就是基础配置加上系列配置,因为
visualMap-continuous
和visualMap-piecewise
都是视觉映射visualMap
只不过是不同type要进行处理一下,同理dataZoom
。 - 另外,为了减少渲染图表的次数,我这里加了个节流,延迟更新。
tsx
export const ChartContent = (props: {
onRef: Ref<{ createChart: () => void }>;
optionsConfig: FormItemValue; //echarts基础配置
seriesConfig: FormItemValue; //echarts系列配置
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chart = useRef<ECharts | null>(null);
const isLock = useRef<boolean>(false);
const createChart = () => {
if (chart.current === null) {
//实例化echarts
chart.current = echarts.init(chartRef.current);
}
//清空图表
chart.current.clear();
if (props.seriesConfig.length === 0) return;
//节流
if (isLock.current) return;
isLock.current = true;
setTimeout(async () => {
// chart.current.showLoading();
const options: any = { ...props.optionsConfig, series: props.seriesConfig };
//配置项处理
if (options['visualMap-continuous']) {
options.visualMap = { ...options['visualMap-continuous'], type: 'continuous' };
delete options['visualMap-continuous'];
}
if (options['visualMap-piecewise']) {
options.visualMap = { ...options['visualMap-piecewise'], type: 'piecewise' };
delete options['visualMap-piecewise'];
}
if (options['dataZoom-inside']) {
options.dataZoom = { ...options['dataZoom-inside'], type: 'inside' };
delete options['dataZoom-inside'];
}
if (options['dataZoom-slider']) {
options.dataZoom = { ...options['dataZoom-slider'], type: 'slider' };
delete options['dataZoom-slider'];
}
//地图注册行政区
const mapSeries = options.series.find((it) => it.type === 'map');
if (mapSeries) {
const geojson = await getGeoJSON(mapSeries.map || '100000');
echarts.registerMap(mapSeries.map, geojson as object);
}
// chart.current.hideLoading();
//设置图表配置
//json反序列化的时候,将特殊的key或value进行处理
chart.current.setOption(
JSON.parse(JSON.stringify(options), (key, value) => {
if (value && typeof value === 'string') {
//字符串转数组
if (['center', 'position', 'radius', 'value', 'data', 'color'].includes(key) && value.indexOf(','))
return value.split(',');
//字符串转函数
if (key === 'formatter' && value.indexOf('function') === 0) return new Function(value).toString();
//字符串转布尔
if (value === 'true') return true;
if (value === 'false') return false;
}
return value;
})
);
console.log('options', options);
isLock.current = false;
}, 500);
};
const onResize = () => {
if (chart.current) {
chart.current.resize();
}
};
//向外暴露方法
useImperativeHandle(props.onRef, () => ({
createChart
}));
//跟随窗口调整大小
useEffect(() => {
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
if (chart.current) chart.current.dispose();
};
}, []);
return (
<div className="chartContent">
<div className="chartBox" ref={chartRef}></div>
<Button style={{ position: 'absolute', top: '0px' }} onClick={createChart} type="primary">
生成图表
</Button>
</div>
);
};
有些需要细化的一些输入类型,如tooltip.formatter
js代码写函数,数值或字符数组等(我这里没有做这些具体的输入类型),可以通过JSON.stringfiy的replacer函数和JSON.parse的reviver函数来转换输入值.
- json序列化的时候,将特殊的key或value进行处理,方便存储
js
JSON.stringify(
{
a: function () {
console.log('hello');
},
b: [1, 2, 3],
c: true,
d: 'hahaha'
},
(key, value) => {
// 数组转字符串
if (['b'].includes(key) && Array.isArray(value) && (typeof value[0] === 'number' || typeof value[0] === 'string'))
return value.join(',');
// 函数转字符串
if (typeof value === 'function') return value.toString();
return value;
}
);
//结果
`{"a":"function () {\n console.log('hello');\n }","b":"1,2,3","c":true,"d":"hahaha"}`
- json反序列化的时候,将特殊的key或value进行处理
js
JSON.parse(
`{
"a": "function(){console.log('hello')}",
"b": "1,2,3",
"c": "true",
"d": "hahaha"
}`,
(key, value) => {
if (value && typeof value === 'string') {
//字符串转数组
if (value.indexOf(',') >= 0) return value.split(',');
//字符串转函数
if (value.indexOf('function') === 0) return new Function('return ' + value)();
//字符串转布尔
if (value === 'true') return true;
if (value === 'false') return false;
}
return value;
}
);
//结果
{
"a":function(){console.log('hello')},
"b": [
"1",
"2",
"3"
],
"c": true,
"d": "hahaha"
}
7.总结
- 整理逻辑就是左侧拖拽动态添加echarts的基础配置和系列配置,右侧生成对应的表单面板,修改表单的属性值同步更新到中间的图表渲染。
- 这个属于开发人员思维的echarts图表配置小工具。如果真正给客户的那种,要删除很多没必要和少用的配置项,排版也不会那么繁杂,需整合属性(即变一个值,其他值自动完成赋值,不需要一个个改),一般都会预置很多echarts的图表样式和主题,并且同样的数据可以用不同类型的图表展示,直接点击切换,实现自动化,减少用户操作。
- 当单纯的用户界面不能满足开发人员对额外属性配置的要求,那么一般都会有个开发环境调试专用的小工具集成在用户界面,比如可以直接修改json的代码编辑器等,echarts的话,我这个也可以试试。
- 我这里没有用redux,因为单向数据流修改某个属性值会触发
object.preventextensions
报错,那么就要cloneDeep一份来修改,这样太消耗性能了。这里只用简单的属性传递和回调更新,通过扁平属性来修改同一个对象或数组的值。对于界面不更新的问题,通过临时的属性值来监听和修改全局的对象和数组,可以做到更新当前修改模块影响的表单面板,大大减少渲染。 - 其他的UI库和图表库只要找到规律,也可以通过脚本自动爬取并转换生成配置表单,然后根据定制化需求,调整就好,工作量比从0开始的手写高效很多。 6.用vue写可能比react简单许多,vue的版本我之前已经做过了,控制逻辑比这个复杂些,这个算是简单练练react。
Github地址
https://github.com/xiaolidan00/echarts-helper
参考: