爬取Echarts官网,搞个图表小工具[React]

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-continuousvisualMap-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.formatterjs代码写函数,数值或字符数组等(我这里没有做这些具体的输入类型),可以通过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.总结

  1. 整理逻辑就是左侧拖拽动态添加echarts的基础配置和系列配置,右侧生成对应的表单面板,修改表单的属性值同步更新到中间的图表渲染。
  2. 这个属于开发人员思维的echarts图表配置小工具。如果真正给客户的那种,要删除很多没必要和少用的配置项,排版也不会那么繁杂,需整合属性(即变一个值,其他值自动完成赋值,不需要一个个改),一般都会预置很多echarts的图表样式和主题,并且同样的数据可以用不同类型的图表展示,直接点击切换,实现自动化,减少用户操作。
  3. 当单纯的用户界面不能满足开发人员对额外属性配置的要求,那么一般都会有个开发环境调试专用的小工具集成在用户界面,比如可以直接修改json的代码编辑器等,echarts的话,我这个也可以试试。
  4. 我这里没有用redux,因为单向数据流修改某个属性值会触发object.preventextensions报错,那么就要cloneDeep一份来修改,这样太消耗性能了。这里只用简单的属性传递和回调更新,通过扁平属性来修改同一个对象或数组的值。对于界面不更新的问题,通过临时的属性值来监听和修改全局的对象和数组,可以做到更新当前修改模块影响的表单面板,大大减少渲染。
  5. 其他的UI库和图表库只要找到规律,也可以通过脚本自动爬取并转换生成配置表单,然后根据定制化需求,调整就好,工作量比从0开始的手写高效很多。 6.用vue写可能比react简单许多,vue的版本我之前已经做过了,控制逻辑比这个复杂些,这个算是简单练练react。

Github地址

https://github.com/xiaolidan00/echarts-helper

参考:

相关推荐
还是大剑师兰特16 分钟前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Watermelo61736 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.9 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖10 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar11 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_8576009512 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_8576009512 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL12 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js