最近在学习react,所以就写了个react+ts的项目。由于其中的大屏模块涉及到了大量echarts的使用,因此封装了一个echarts基础组件
直接上代码
第一部分 引入模块
import * as echarts from 'echarts/core';
import React, { FC } from 'react';
import {
DatasetComponent,
DatasetComponentOption,
DataZoomComponent,
DataZoomComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
TitleComponent,
TitleComponentOption,
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption,
MarkLineComponent,
PolarComponent,
AxisPointerComponentOption,
AriaComponentOption,
AriaComponent,
AxisPointerComponent,
BrushComponent,
BrushComponentOption,
CalendarComponent,
CalendarComponentOption,
VisualMapComponent,
} from 'echarts/components';
import {
BarChart,
BarSeriesOption,
LineChart,
LineSeriesOption,
PieChart,
PictorialBarChart,
GaugeChart,
PieSeriesOption,
PictorialBarSeriesOption,
GaugeSeriesOption,
MapChart,
} from 'echarts/charts';
import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared';
import { UniversalTransition } from 'echarts/features';
import { SVGRenderer } from 'echarts/renderers';
import { useEffect, useRef } from 'react';
import { useSize } from 'ahooks';
import { debounce, isArray, isNumber, isPlainObject, merge } from 'lodash-es';
import { getScale } from '@/utils/global';
import { Empty } from 'antd';
echarts.use([
MapChart,
PieChart,
BarChart,
LineChart,
GaugeChart,
GridComponent,
TitleComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
SVGRenderer,
ToolboxComponent,
MarkLineComponent,
PolarComponent,
PictorialBarChart,
DatasetComponent,
UniversalTransition,
AriaComponent,
BrushComponent,
CalendarComponent,
AxisPointerComponent,
VisualMapComponent,
]);
export type MyChartOption = echarts.ComposeOption<
| DatasetComponentOption
| DataZoomComponentOption
| GridComponentOption
| LegendComponentOption
| TitleComponentOption
| ToolboxComponentOption
| TooltipComponentOption
| LineSeriesOption
| BarSeriesOption
| PieSeriesOption
| PictorialBarSeriesOption
| GaugeSeriesOption
| AxisPointerComponentOption
| AriaComponentOption
| BrushComponentOption
| CalendarComponentOption
| XAXisOption
| YAXisOption
>;
export interface MyChartProps {
option: MyChartOption;
isTransform?: boolean;
isMap?: boolean;
json?: any[];
mapName?: string;
}
export type symbolType = string | [string, string] | undefined;
这里提几个注意的点,就是XAXisOption, YAXisOption是文档中没有的,但是我在实际使用时一直提示说XAXisOption, YAXisOption类型不匹配,因此找了好久才找到这两个类型。还有就是在使用echarts画地图时,要注意引入MapChart, VisualMapComponent这两个部分。
第二部分 封装组件
基本的思路如下:
-
提供默认配置,在初始化时会和传入的配置合并
-
判断是否为空,为空则不生成echarts
-
转换Formatter, 属于定制功能,可要可不要,用来统一echarts中tooltip的效果
-
transformKeys转换,即将一些属性,比如字体,根据大屏的比例进行一定程度的缩放
-
onTransformOptions对传入的options进行转换,比如上面的转换Formatter和transformKeys就在这个方法里调用,也可以不调用
-
initChart初始化echarts
- 判断是否已经有了,有则销毁
- 判断是否有传入option或者挂载的节点生成了,有一个没有则不生成echarts
- 判断是否为空,为空则返回
- 合并默认配置和传入的option
- 判断是否为地图,为地图则调用registerMap方法
- 监听数据变化,重置echarts
- 监听窗口大小,重置echarts大小
基本思路就是这么些,下面是具体代码:
// 默认配置
const defaultOptions = {
tooltip: {
textStyle: {
fontSize: 14,
},
},
legend: {
itemWidth: 25,
itemHeight: 14,
pageIconColor: 'currentColor',
textStyle: { fontSize: 14, color: 'currentColor' },
pageTextStyle: { fontSize: 14, color: 'currentColor' },
},
xAxis: {
nameGap: 15,
nameTextStyle: {
fontSize: 14,
},
axisLabel: {
fontSize: 14,
},
},
yAxis: {
nameGap: 15,
nameTextStyle: {
fontSize: 14,
},
axisLabel: {
fontSize: 14,
},
},
};
const MyChart: React.FC<MyChartProps> = ({ option, isTransform, isMap = false, json = [], mapName = '' }) => {
const cRef = useRef<HTMLDivElement>(null); // 挂载的dom
const cInstance = useRef<echarts.ECharts>(); // echarts实例
// 判断是否为空
const isEmpty = () => {
if (!option) return false;
if (!isArray(option.series)) return !option.series?.data?.length;
return !option.series.length || !option.series[0].data?.length;
};
// 转换tooltip的Formatter 属于是定制了 可自由更改 也可删除
const transformFormatter = (value: Array<{ key: string; unit?: string; default?: string }>) => () => {
return (params: any) => {
const style = 'padding-left:0.2rem;text-align:right;font-weight:900;';
const title = `<p style="font-size:1.1em;line-height:1.5;">${params[0].name}</p>`;
const content = params.map((item: { marker: any; seriesName: any; data: { [x: string]: any } }) => {
const items = [
`<td>${item.marker}</td>`,
`<td>${item.seriesName}</td>`,
...value.map((prop) => `<td style="${style}">${item.data[prop.key] ?? prop.default}${prop.unit || ''}</td>`),
];
return `<tr>${items.join('')}</tr>`;
});
return `${title}<table>${content.join('')}</table>`;
};
};
// transformKeys 是用来按照一定比例缩放这些属性 保证能在大屏中正常显示
const transformKeys = ['fontSize', 'itemWidth', 'itemHeight', 'right', 'bottom', 'top', 'left', 'width', 'shadowBlur'];
// 转换options
const onTransformOptions = (options: any) => {
// 不转换
if (!isTransform) return options;
// 数组
if (isArray(options)) {
options.forEach(onTransformOptions);
return options;
}
// 非对象【字符串、函数】
if (!isPlainObject(options)) return options;
// 对象
for (const key in options) {
if (key === 'formatter' && isArray(options[key])) {
options[key] = transformFormatter(options[key]);
} else if (transformKeys.includes(key) && isNumber(options[key])) {
options[key] = getScale(options[key]);
} else {
options[key] = onTransformOptions(options[key]);
}
}
return options;
};
// 初始化initChart
const initChart = () => {
if (cInstance.current) cInstance.current.dispose(); // 判断是否有了
if (!cRef || !option) return; // 判断是否生成
if (isEmpty()) return; // 判断是否为空
cInstance.current = echarts.init(cRef.current);
const options = merge({}, defaultOptions, option); // 将传入的options合并默认配置
for (const key in defaultOptions) {
if (!option[key]) delete options[key];
}
// 是否为地图
if (isMap) {
// @ts-expect-error: TS1234 because the library definition is wrong
echarts.registerMap(mapName, json); // MapInput GeoJSONSourceInput
}
cInstance.current.setOption(onTransformOptions(options));
const series = isArray(option.series) ? option.series : [option.series];
const seriesIndex = series.findIndex((item) => item && item.type === 'pie');
if (seriesIndex >= 0) {
let index = 0;
cInstance.current.dispatchAction({ type: 'highlight', seriesIndex, dataIndex: index });
cInstance.current.on('mouseover', function (e) {
if (index === e.dataIndex) return;
cInstance.current && cInstance.current.dispatchAction({ type: 'downplay', seriesIndex, dataIndex: index });
cInstance.current && cInstance.current.dispatchAction({ type: 'highlight', seriesIndex, dataIndex: e.dataIndex });
index = e.dataIndex;
});
}
};
// 监听数据变化,重置图表
useEffect(() => {
initChart();
}, [cRef, option]);
// 监听窗口大小变化,重置图表大小
const resize = debounce(() => {
cInstance.current && cInstance.current.resize();
}, 200);
const size = useSize(cRef);
useEffect(() => {
resize();
}, [size]);
return (
!isEmpty() ? <div ref={cRef} style={{ width: '100%', height: '100%' }} /> : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)
);
};
export default MyChart;
给个示例:
const ChinaMap: FC = () => {
const [data, setData] = useState<any[]>([]);
useEffect(() => {
const fn = async () => {
const response = await fetch('/JSON/china.json'); // 使用相对路径引用JSON文件
const data = await response.json();
setData(data);
};
fn();
}, []);
const option = {
tooltip: {
//鼠标hover是提示信息
show: true,
formatter: '{b}', //提示标签格式
backgroundColor: '#ff7f50', //提示标签背景颜色
textStyle: {
color: '#fff',
fontSize: '20',
}, //提示标签字体颜色
},
visualMap: {
//视觉映射组件()
type: 'continuous', //连续型
min: 0,
max: 150,
right: 120,
bottom: 140,
text: ['150', '0'], // 文本,默认为数值文本
textGap: 10, //文本与图形之间的距离
itemWidth: 40, //图形的宽
itemHeight: 200, //图形的高
calculable: true, //是否显示拖动手柄
textStyle: {
color: '#fff',
fontSize: 25,
}, //省份标签字体颜色
//align:"left",
//inverse: true, //反向
inRange: {
//地图颜色变化
color: ['#3246FB', '#24DD57', '#FDD52C'],
},
// outOfRange:{
// symbolSize: [100, 100]
// }
},
series: [
{
type: 'map',
color: 'red',
mapType: 'china',
roam: 'false', //是否开启缩放平移
label: {
//标签字体样式
position: 'inside',
show: true, //显示省份标签
textStyle: {
color: '#fff',
fontSize: 20,
}, //省份标签字体颜色
emphasis: {
//对应的鼠标悬浮效果
show: true,
textStyle: {
color: '#800080',
},
},
},
itemStyle: {
borderWidth: 2, //区域边框宽度
borderColor: '#fff', //区域边框颜色
//areaColor: "#ffefd5", //区域颜色
fontSize: 30,
},
emphasis: {
borderWidth: 2,
borderColor: '#3246FB', // 边框颜色
// areaColor: "#6AE5E5", // 区域颜色
},
data: CITIES.map((item) => {
const value = Math.floor(Math.random() * 200);
return {
name: item,
value,
};
}),
},
],
} as MyChartOption;
return (
<div className={styles.ChinaMap}>
<MyChart option={option} isMap={true} json={data} mapName="china" />
</div>
);
};
如有任何问题,欢迎指正
这个项目目前用到技术点为: react + ts + echarts + 高德地图 + webrtc + Nest.js + next.js 我打算把这个项目打造成一个缝合怪,将所学的技术全都融合进去,做成一个大杂烩。预计花费一年时间,每个月出一个大版本,加入一些新东西。 这个react学习项目的地址是这里,欢迎大家前来围观。也可以加入到这个项目中来,一起学习进步,将这个项目打造成自己的开源项目。