|
|
@@ -1,869 +1,262 @@
|
|
|
-import React, { useEffect, useRef, useState } from 'react'
|
|
|
-// import * as echarts from 'echarts'
|
|
|
-import { EChartsOption, init, dispose, registerMap } from 'echarts'
|
|
|
-import { Empty } from 'antd'
|
|
|
-import { china } from '../utils/dictionary'
|
|
|
-/**通用直接传入原始配置EChartsOption*/
|
|
|
-function Echarts(props: { children: EChartsOption | null, style?: { width: number | string, height: number | string } }) {
|
|
|
- const { style } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- /**创建myChart实例 */
|
|
|
- useEffect(() => {
|
|
|
- let myChart:any = null
|
|
|
- if (ref?.current && props?.children) {
|
|
|
+import React, { useEffect, useRef } from 'react';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import { Empty } from 'antd';
|
|
|
|
|
|
- myChart = init(ref.current)
|
|
|
- setMyChart(myChart)
|
|
|
- myChart.setOption(props.children);
|
|
|
- myChart.resize()
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, props?.children])
|
|
|
- /**卸载myChart */
|
|
|
- useEffect(() => {
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [myChart?.id])
|
|
|
- return <>
|
|
|
- {
|
|
|
- props.children
|
|
|
- ?
|
|
|
- <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
- :
|
|
|
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
-}
|
|
|
-/**
|
|
|
- * Map粉丝统计
|
|
|
- * @param data name省名称 vlaue值
|
|
|
- * @param style width ,height
|
|
|
-*/
|
|
|
-function Map(props: { data: { name: string, value: number }[], style?: { width: number | string, height: number | string } }) {
|
|
|
- const { data, style } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- /**创建myChart实例 */
|
|
|
- useEffect(() => {
|
|
|
- let myChart:any = null
|
|
|
- if (ref?.current && data.length > 0) {
|
|
|
- myChart = init(ref.current)//初始化chart
|
|
|
- registerMap('China', require('../public/map.json'))//获取地图数据
|
|
|
- let num: number[] = []//获取所有值方便取最大最小值
|
|
|
- let newData: any = china//处理匹配接口返回数据和地图数据的映射避免名字不同
|
|
|
- data.forEach((item: { name: string, value: number }) => {
|
|
|
- newData.forEach((data: { name: string, value: number }) => {
|
|
|
- if (item.name && data.name.indexOf(item.name) !== -1) {
|
|
|
- data.value = item.value
|
|
|
- num.push(item.value)
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
- setMyChart(myChart)//存放Chart实例以备卸载
|
|
|
- myChart.setOption({//设置Chart配置
|
|
|
- /**悬浮提示 */
|
|
|
- tooltip: {
|
|
|
- trigger: 'item',
|
|
|
- formatter: '{b}:{c}'
|
|
|
- },
|
|
|
- /**右侧操作工具 */
|
|
|
- toolbox: {
|
|
|
- show: true,
|
|
|
- orient: 'vertical',
|
|
|
- right: 50,
|
|
|
- top: 'center',
|
|
|
- feature: {
|
|
|
- dataView: {
|
|
|
- readOnly: false,
|
|
|
- title: '查看详情'
|
|
|
- },
|
|
|
- restore: {
|
|
|
- title: '刷新'
|
|
|
- },
|
|
|
- saveAsImage: {
|
|
|
- title: '保存为图片'
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- /**左侧操作工具 */
|
|
|
- visualMap: {
|
|
|
- min: 0,
|
|
|
- max: Math.max(...num),
|
|
|
- text: ['High', 'Low'],
|
|
|
- realtime: false,
|
|
|
- calculable: true,
|
|
|
- top: 'middle',
|
|
|
- inRange: {
|
|
|
- color: ['lightskyblue', 'yellow', 'orangered']
|
|
|
- }
|
|
|
- },
|
|
|
- /**地图参数设置 */
|
|
|
- series: [
|
|
|
- {
|
|
|
- name: '粉丝统计',
|
|
|
- type: 'map',
|
|
|
- map: 'China', // 自定义扩展图表类型
|
|
|
- zoom: 1.2,
|
|
|
- scaleLimit: { min: 1, max: 10 },
|
|
|
- roam: true,
|
|
|
- nameProperty: 'name',
|
|
|
- data: newData,
|
|
|
- }
|
|
|
- ],
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data])
|
|
|
- /**卸载myChart */
|
|
|
+/* =====================================================
|
|
|
+ 🔧 通用初始化 Hook (核心)
|
|
|
+===================================================== */
|
|
|
+const useEchartInit = (theme?: string | object) => {
|
|
|
+ const chartRef = useRef<HTMLDivElement>(null);
|
|
|
+ const chartInstance = useRef<echarts.ECharts | null>(null);
|
|
|
+
|
|
|
+ // 初始化 ECharts 实例(只执行一次)
|
|
|
useEffect(() => {
|
|
|
- if (data.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
+ if (!chartRef.current) return;
|
|
|
+ const chart = echarts.init(chartRef.current, theme);
|
|
|
+ chartInstance.current = chart;
|
|
|
+
|
|
|
+ const handleResize = () => chart.resize();
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+
|
|
|
return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- data.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
- }
|
|
|
- {
|
|
|
- data.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+ chart.dispose();
|
|
|
+ chartInstance.current = null;
|
|
|
+ };
|
|
|
+ }, [theme]);
|
|
|
+
|
|
|
+ return { chartRef, chartInstance };
|
|
|
+};
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 🎨 默认颜色色板
|
|
|
+===================================================== */
|
|
|
+const DEFAULT_COLORS = [
|
|
|
+ '#5470C6', '#91CC75', '#EE6666', '#FAC858',
|
|
|
+ '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC'
|
|
|
+];
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 📊 通用 Echarts 容器组件
|
|
|
+===================================================== */
|
|
|
+interface EchartsProps {
|
|
|
+ option?: echarts.EChartsOption | null;
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ theme?: string | object;
|
|
|
+ loading?: boolean;
|
|
|
}
|
|
|
-/**
|
|
|
- * 饼图总关注
|
|
|
- * @param data name 名称 value 值
|
|
|
- * @param style width,height
|
|
|
- * @param name tooltip上的名字即饼图的名字
|
|
|
- * @param centerName 饼图中心统计的名称
|
|
|
-*/
|
|
|
-function PieFocus(props: {
|
|
|
- /**
|
|
|
- * @param data name 名称 value 值
|
|
|
- */
|
|
|
- data: { name: string, value: number }[],
|
|
|
- /**
|
|
|
- * @param name tooltip上的名字即饼图的名字
|
|
|
- */
|
|
|
- name: string,
|
|
|
- /**
|
|
|
- * @param centerName 饼图中心统计的名称
|
|
|
- */
|
|
|
- centerName: string,
|
|
|
- /**
|
|
|
- * @param style width,height
|
|
|
- */
|
|
|
- style?: { width: number | string, height: number | string },
|
|
|
-}) {
|
|
|
- const { data, style, name, centerName } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- /**创建myChart实例 */
|
|
|
- useEffect(() => {
|
|
|
- let myChart: any = null
|
|
|
- if (ref?.current && data.length > 0) {
|
|
|
- myChart = init(ref.current)
|
|
|
- setMyChart(myChart)
|
|
|
- myChart.setOption({
|
|
|
- /**鼠标悬浮 */
|
|
|
- tooltip: {
|
|
|
- // trigger: 'item',
|
|
|
- // formatter: '{a} <br />{b}: {c} ({d}%)'
|
|
|
- },
|
|
|
- /**图列 */
|
|
|
- legend: {
|
|
|
- left: 'center',
|
|
|
- bottom: 0,
|
|
|
- data: data?.map((item: { name: string }) => item.name)
|
|
|
- },
|
|
|
- /**工具保存为图片 */
|
|
|
- toolbox: {
|
|
|
- right: 50,
|
|
|
- feature: {
|
|
|
- saveAsImage: {
|
|
|
- title: '保存为图片'
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- /**饼图参数 */
|
|
|
- series: [
|
|
|
- {
|
|
|
- name,
|
|
|
- type: 'pie',
|
|
|
- radius: ['50%', '70%'],
|
|
|
- // avoidLabelOverlap: false,
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- // position: 'center',
|
|
|
- // fontSize: '30',
|
|
|
- // fontWeight: 'bold',
|
|
|
- formatter: `{b}:{c} ({d}%)`,
|
|
|
- },
|
|
|
- data
|
|
|
- },
|
|
|
- {
|
|
|
- name,
|
|
|
- type: 'pie',
|
|
|
- radius: ['50%', '70%'],
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- position: 'center',
|
|
|
- fontSize: '30',
|
|
|
- fontWeight: 'bold',
|
|
|
- formatter: `${data.length > 1 ? data?.reduce((a: any, b: { value: number }) => {
|
|
|
- return (a?.value || a) + (b?.value || 0)
|
|
|
- }) : data[0].value}\n${centerName}`,
|
|
|
- },
|
|
|
- data
|
|
|
- }
|
|
|
- ]
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data])
|
|
|
- /**卸载myChart */
|
|
|
+
|
|
|
+const Echarts: React.FC<EchartsProps> = ({ option, style = { width: '100%', height: '100%' }, theme, loading }) => {
|
|
|
+ const { chartRef, chartInstance } = useEchartInit(theme);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
- if (data.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
+ if (!chartInstance.current) return;
|
|
|
+ if (!option) {
|
|
|
+ chartInstance.current.clear();
|
|
|
+ return;
|
|
|
}
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- data.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
- }
|
|
|
- {
|
|
|
- data.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }} >
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
+
|
|
|
+ loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
|
|
|
+ chartInstance.current.setOption(option, true);
|
|
|
+ }, [option, loading]);
|
|
|
+
|
|
|
+ return <div ref={chartRef} style={style} />;
|
|
|
+};
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 📈 折线图 Line
|
|
|
+===================================================== */
|
|
|
+export interface LineData {
|
|
|
+ legendName: string;
|
|
|
+ [key: string]: any;
|
|
|
}
|
|
|
-/**
|
|
|
- * Line 折线图
|
|
|
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
|
|
|
- * @param style width,height
|
|
|
-*/
|
|
|
-function Line(props: {
|
|
|
- data?: { legendName: string, [key: string]: any }[],
|
|
|
- style?: React.CSSProperties,
|
|
|
- areaStyle?: boolean,//线变块颜色
|
|
|
- color?: string | string[],//线颜色
|
|
|
- markPoint?: any,//浮点
|
|
|
- fontColor?: string,//文字颜色
|
|
|
- series?: boolean,//堆叠
|
|
|
- title?: string,//标题
|
|
|
- smooth?: boolean,//true 圆弧线 false 直角
|
|
|
-}) {
|
|
|
- const { data, style, areaStyle = false, color, markPoint, fontColor, series, title, smooth = false } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
|
|
|
- /**创建myChart实例 */
|
|
|
- useEffect(() => {
|
|
|
- let myChart: any = null
|
|
|
- if (ref?.current && (data as any[]).length > 0) {
|
|
|
- let Xdata: any = []
|
|
|
- let Ydatas: any[] = []
|
|
|
- data?.forEach((item, index) => {
|
|
|
- Ydatas.push([])
|
|
|
- Object.keys(item).forEach((key: string) => {
|
|
|
- if (index === 0 && key !== 'legendName') {
|
|
|
- Xdata.push(key)
|
|
|
- }
|
|
|
- if (key !== 'legendName') {
|
|
|
- Ydatas[index].push(item[key])
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- myChart = init(ref.current)
|
|
|
- setMyChart(myChart)
|
|
|
- myChart.setOption({
|
|
|
- /**鼠标悬浮 */
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis'
|
|
|
- },
|
|
|
- /**图列 */
|
|
|
- legend: {
|
|
|
- data: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }) => item.legendName),
|
|
|
- bottom: 0,
|
|
|
- left: 'center',
|
|
|
- ...textStyle
|
|
|
- },
|
|
|
- xAxis: {
|
|
|
- data: Xdata,
|
|
|
- boundaryGap: false,//贴边
|
|
|
- },
|
|
|
- yAxis: [
|
|
|
- {
|
|
|
- type: 'value',
|
|
|
- },
|
|
|
- ],
|
|
|
- grid: {
|
|
|
- left: 100,
|
|
|
- right: 50,
|
|
|
- },
|
|
|
- ...textStyle,
|
|
|
- /**工具保存为图片 */
|
|
|
- toolbox: {
|
|
|
- right: 50,
|
|
|
- feature: {
|
|
|
- saveAsImage: {
|
|
|
- title: '保存为图片'
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- title: {
|
|
|
- text: title || '',
|
|
|
- textAlign: 'center',
|
|
|
- right: 'center'
|
|
|
- },
|
|
|
- /**线图参数 */
|
|
|
- series: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }, index: number) => {
|
|
|
- return {
|
|
|
- name: item.legendName,//名称
|
|
|
- type: 'line',//图形
|
|
|
- data: Ydatas[index],//数据
|
|
|
- areaStyle: areaStyle ? {} : undefined,//是否开启块级颜色
|
|
|
- color: Array.isArray(color) ? color[index] : color,//颜色
|
|
|
- markPoint: {//最大值最小值
|
|
|
- data: markPoint ? markPoint : []
|
|
|
- },
|
|
|
- emphasis: series ? { focus: 'series' } : {}, //堆叠
|
|
|
- smooth
|
|
|
- }
|
|
|
- })
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data])
|
|
|
- /**卸载myChart */
|
|
|
- useEffect(() => {
|
|
|
- if ((data as any[])?.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
|
|
|
- }
|
|
|
- {
|
|
|
- (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
+interface LineProps {
|
|
|
+ data?: LineData[];
|
|
|
+ title?: string;
|
|
|
+ smooth?: boolean;
|
|
|
+ areaStyle?: boolean;
|
|
|
+ seriesFocus?: boolean;
|
|
|
+ color?: string[];
|
|
|
+ fontColor?: string;
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ loading?: boolean;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Bar 柱状图
|
|
|
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
|
|
|
- * @param style width,height
|
|
|
-*/
|
|
|
-function Bar(props: {
|
|
|
- data?: { legendName: string, [key: string]: any }[],
|
|
|
- style?: React.CSSProperties,
|
|
|
- fontColor?: string,//文字颜色
|
|
|
- title?: string,//标题
|
|
|
-}) {
|
|
|
- const { data, style, fontColor, title } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
|
|
|
- /**创建myChart实例 */
|
|
|
- useEffect(() => {
|
|
|
- let myChart: any = null
|
|
|
- if (ref?.current && (data as any[]).length > 0) {
|
|
|
- let Xdata: any = [] // name
|
|
|
- let Ydatas: any[] = [] // value
|
|
|
- data?.forEach((item) => {
|
|
|
- Xdata.push(item.name)
|
|
|
- Ydatas.push(item.value)
|
|
|
- })
|
|
|
- let sum = eval(Ydatas.join("+"))
|
|
|
- Ydatas = Ydatas.map((item: number) => {
|
|
|
- return (item / sum * 100).toFixed(0)
|
|
|
- })
|
|
|
- myChart = init(ref.current)
|
|
|
- setMyChart(myChart)
|
|
|
- myChart.setOption({
|
|
|
- /**鼠标悬浮 */
|
|
|
- // tooltip: {
|
|
|
- // trigger: 'axis',
|
|
|
- // },
|
|
|
- xAxis: {
|
|
|
- max: 'dataMax'
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'category',
|
|
|
- data: Xdata,
|
|
|
- inverse: true
|
|
|
- },
|
|
|
- grid: {
|
|
|
- left: 100,
|
|
|
- right: 50,
|
|
|
- },
|
|
|
- ...textStyle,
|
|
|
- title: {
|
|
|
- text: title || '',
|
|
|
- textAlign: 'center',
|
|
|
- right: 'center'
|
|
|
- },
|
|
|
- /**线图参数 */
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'bar',//图形
|
|
|
- realtimeSort: true,
|
|
|
- data: Ydatas,//数据
|
|
|
- name: 'X',
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- position: 'right',
|
|
|
- valueAnimation: true,
|
|
|
- }
|
|
|
- }
|
|
|
- ]
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data])
|
|
|
- /**卸载myChart */
|
|
|
+const Line: React.FC<LineProps> = ({
|
|
|
+ data = [],
|
|
|
+ title,
|
|
|
+ smooth = false,
|
|
|
+ areaStyle = false,
|
|
|
+ seriesFocus = false,
|
|
|
+ color = DEFAULT_COLORS,
|
|
|
+ fontColor,
|
|
|
+ style = { width: '100%', height: 400 },
|
|
|
+ loading = false
|
|
|
+}) => {
|
|
|
+ const { chartRef, chartInstance } = useEchartInit();
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
- if ((data as any[])?.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
+ if (!chartInstance.current) return;
|
|
|
+ if (!data.length) {
|
|
|
+ chartInstance.current.clear();
|
|
|
+ return;
|
|
|
}
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
|
|
|
- }
|
|
|
- {
|
|
|
- (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
+ const textStyle = fontColor ? { color: fontColor } : {};
|
|
|
+ const xData = Object.keys(data[0]).filter(k => k !== 'legendName');
|
|
|
+ const seriesData = data.map((item, i) => ({
|
|
|
+ name: item.legendName,
|
|
|
+ type: 'line',
|
|
|
+ data: xData.map(k => item[k]),
|
|
|
+ smooth,
|
|
|
+ areaStyle: areaStyle ? {} : undefined,
|
|
|
+ emphasis: seriesFocus ? { focus: 'series' } : undefined
|
|
|
+ }));
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ color,
|
|
|
+ title: { text: title, left: 'center', textStyle },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ legend: { data: data.map(i => i.legendName), bottom: 0, textStyle },
|
|
|
+ xAxis: { type: 'category', data: xData, boundaryGap: false },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ grid: { left: 60, right: 40, bottom: 80 },
|
|
|
+ series: seriesData as any
|
|
|
+ };
|
|
|
+
|
|
|
+ loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
|
|
|
+ chartInstance.current.setOption(option, true);
|
|
|
+ }, [data, title, smooth, areaStyle, color, fontColor, seriesFocus, loading]);
|
|
|
+
|
|
|
+ return <div ref={chartRef} style={style} />;
|
|
|
+};
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 🥧 饼图 Pie
|
|
|
+===================================================== */
|
|
|
+interface PieProps {
|
|
|
+ data?: { name: string; value: number }[];
|
|
|
+ title?: string;
|
|
|
+ color?: string[];
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ loading?: boolean;
|
|
|
}
|
|
|
|
|
|
+const Pie: React.FC<PieProps> = ({
|
|
|
+ data = [],
|
|
|
+ title,
|
|
|
+ color = DEFAULT_COLORS,
|
|
|
+ style = { width: '100%', height: 400 },
|
|
|
+ loading = false
|
|
|
+}) => {
|
|
|
+ const { chartRef, chartInstance } = useEchartInit();
|
|
|
|
|
|
-/**
|
|
|
- * Bar 柱状图
|
|
|
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
|
|
|
- * @param style width,height
|
|
|
-*/
|
|
|
-// [
|
|
|
-// { name: '4341556373', value: 300 }, { name: '4341523456', value: 240 },
|
|
|
-// ]
|
|
|
-function BarMonitor(props: {
|
|
|
- data?: { legendName: string, [key: string]: any }[],
|
|
|
- style?: React.CSSProperties,
|
|
|
- fontColor?: string,//文字颜色
|
|
|
- title?: string,//标题
|
|
|
- isGraphic?: boolean, // 是否渐变
|
|
|
- xName?: string,
|
|
|
- yName?: string,
|
|
|
- planID?: string,
|
|
|
- onChange?: (id?: string) => void,
|
|
|
-}) {
|
|
|
- const { data, style, fontColor, title, xName, yName, planID, onChange } = props
|
|
|
- const colors = ['#f8128d', '#fa40a3', '#f56db5', '#f58bc4', '#f7a1cf', '#f5b7d8', '#facee5', '#f8c8e2', '#f8d6e8', '#fae8f1']
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
|
|
|
-
|
|
|
- /**创建myChart实例 */
|
|
|
useEffect(() => {
|
|
|
- let myChart: any = null
|
|
|
- if (ref?.current && (data as any[]).length > 0) {
|
|
|
- let Xdata: any = [] // name
|
|
|
- let Ydatas: any[] = [] // value
|
|
|
- data?.forEach((item) => {
|
|
|
- Xdata.push(item.adName)
|
|
|
- Ydatas.push(item.value)
|
|
|
- })
|
|
|
- let dataIndex = data?.findIndex((item: any) => item?.name == planID)
|
|
|
- Ydatas = Ydatas.map((item: number, index: number) => {
|
|
|
- if (dataIndex !== -1 && dataIndex === index) {
|
|
|
- return {
|
|
|
- value: item,
|
|
|
- itemStyle: {
|
|
|
- color: '#37A2FF',
|
|
|
- shadowColor: 'rgba(0, 0, 0, 0.8)',
|
|
|
- shadowBlur: 10,
|
|
|
- },
|
|
|
- }
|
|
|
- } else {
|
|
|
- return {
|
|
|
- value: item,
|
|
|
- itemStyle: {
|
|
|
- color: colors[index],
|
|
|
- }
|
|
|
+ if (!chartInstance.current) return;
|
|
|
+ if (!data.length) {
|
|
|
+ chartInstance.current.clear();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ color,
|
|
|
+ title: { text: title, left: 'center' },
|
|
|
+ tooltip: { trigger: 'item' },
|
|
|
+ legend: { bottom: 0, left: 'center' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: '60%',
|
|
|
+ data,
|
|
|
+ label: { formatter: '{b}: {d}%', color: '#333' },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.3)' }
|
|
|
}
|
|
|
}
|
|
|
+ ]
|
|
|
+ };
|
|
|
|
|
|
- })
|
|
|
- myChart = init(ref.current)
|
|
|
- setMyChart(myChart)
|
|
|
- myChart?.setOption({
|
|
|
- /**鼠标悬浮 */
|
|
|
- // tooltip: {
|
|
|
- // trigger: 'axis',
|
|
|
- // },
|
|
|
- xAxis: {
|
|
|
- name: xName || '',
|
|
|
- max: 'dataMax',
|
|
|
- nameTextStyle: { // y轴名字文字样式设置
|
|
|
- verticalAlign: "bottom",
|
|
|
- lineHeight: 5,
|
|
|
- fontWeight: 'bold',
|
|
|
- },
|
|
|
- axisLine: { // 坐标轴轴线相关设置
|
|
|
- show: true // 是否显示坐标轴轴线
|
|
|
- },
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- name: yName || '',
|
|
|
- type: 'category',
|
|
|
- data: Xdata,
|
|
|
- inverse: true,
|
|
|
- nameLocation: 'start', // y轴名字显示位置
|
|
|
- nameTextStyle: { // y轴名字文字样式设置
|
|
|
- verticalAlign: "bottom",
|
|
|
- lineHeight: 0,
|
|
|
- fontWeight: 'bold',
|
|
|
- color: '#000',
|
|
|
- padding: [0, 80, 0, 0]
|
|
|
- },
|
|
|
- nameGap: 20,
|
|
|
- triggerEvent: true,
|
|
|
- axisLabel: {//名称
|
|
|
- margin: 10,
|
|
|
- width: 80,
|
|
|
- overflow: 'truncate',
|
|
|
- ellipsis: '...',
|
|
|
- color: (value: any, index: any) => {
|
|
|
- if (index === dataIndex) {
|
|
|
- return '#f8128d'
|
|
|
- }
|
|
|
- return '#3946c3'
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- grid: {
|
|
|
- left: 100,
|
|
|
- right: 70
|
|
|
- },
|
|
|
- ...textStyle,
|
|
|
- title: {
|
|
|
- text: title || '起量广告排行榜',
|
|
|
- textStyle: {
|
|
|
- color: '#3946c3',
|
|
|
- fontSize: 16
|
|
|
- },
|
|
|
- left: '45%',
|
|
|
- top: 330,
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- show: true,
|
|
|
- // formatter:'{b0}: {c0}',
|
|
|
- formatter: function (a: any) {
|
|
|
- return `${a.marker} <strong> ${a.name} : ${a.value}</strong>`
|
|
|
- }
|
|
|
- },
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'bar',//图形
|
|
|
- realtimeSort: true,
|
|
|
- data: Ydatas,//数据
|
|
|
- name: 'X',
|
|
|
- showBackground: true,
|
|
|
- backgroundStyle: {
|
|
|
- color: 'transparent'
|
|
|
- },
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- valueAnimation: true,
|
|
|
- position: 'right',
|
|
|
- fontSize: 14,
|
|
|
- fontWeight: 900
|
|
|
- }
|
|
|
- }
|
|
|
- ]
|
|
|
- });
|
|
|
- myChart.on('click', (params: any) => {
|
|
|
- let v = params.dataIndex
|
|
|
- let d: any = (data as any[])[v]
|
|
|
- onChange && onChange(d.name)
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data, planID])
|
|
|
- /**卸载myChart */
|
|
|
- useEffect(() => {
|
|
|
- if ((data as any[])?.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
+ loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
|
|
|
+ chartInstance.current.setOption(option, true);
|
|
|
+ }, [data, title, color, loading]);
|
|
|
|
|
|
- }
|
|
|
- {
|
|
|
- (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
+ return <div ref={chartRef} style={style} />;
|
|
|
+};
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 📊 柱状图 Bar(支持横向)
|
|
|
+===================================================== */
|
|
|
+interface BarProps {
|
|
|
+ data?: { name: string; value: number }[];
|
|
|
+ title?: string;
|
|
|
+ color?: string[];
|
|
|
+ horizontal?: boolean;
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ loading?: boolean;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Line 折线图
|
|
|
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
|
|
|
- * @param style width,height
|
|
|
-*/
|
|
|
-function LineMonitor(props: {
|
|
|
- data?: { legendName: string, [key: string]: any }[],
|
|
|
- style?: React.CSSProperties,
|
|
|
- areaStyle?: boolean,//线变块颜色
|
|
|
- color?: string | string[],//线颜色
|
|
|
- markPoint?: any,//浮点
|
|
|
- fontColor?: string,//文字颜色
|
|
|
- series?: boolean,//堆叠
|
|
|
- title?: string,//标题
|
|
|
- smooth?: boolean,//true 圆弧线 false 直角
|
|
|
- dataZoomInside?: boolean, // 控制是否开启滚轮缩放
|
|
|
- dataZoomSlider?: boolean, // 控制是否开启组件缩放
|
|
|
-}) {
|
|
|
- const { data, style, areaStyle = false, color, markPoint, fontColor, series, title, smooth = false, dataZoomInside, dataZoomSlider } = props
|
|
|
- const ref: { current: HTMLDivElement | null } = useRef(null)
|
|
|
- const [myChart, setMyChart] = useState<any>()
|
|
|
- let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
|
|
|
- /**创建myChart实例 */
|
|
|
+const Bar: React.FC<BarProps> = ({
|
|
|
+ data = [],
|
|
|
+ title,
|
|
|
+ color = DEFAULT_COLORS,
|
|
|
+ horizontal = false,
|
|
|
+ style = { width: '100%', height: 400 },
|
|
|
+ loading = false
|
|
|
+}) => {
|
|
|
+ const { chartRef, chartInstance } = useEchartInit();
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
- let myChart: any = null
|
|
|
- if (ref?.current && (data as any[]).length > 0) {
|
|
|
- let Xdata: any = []
|
|
|
- let Ydatas: any[] = []
|
|
|
- data?.forEach((item, index) => {
|
|
|
- Ydatas.push([])
|
|
|
- Object.keys(item).forEach((key: string) => {
|
|
|
- if (index === 0 && key !== 'legendName') {
|
|
|
- Xdata.push(key)
|
|
|
- }
|
|
|
- if (key !== 'legendName') {
|
|
|
- Ydatas[index].push(item[key])
|
|
|
+ if (!chartInstance.current) return;
|
|
|
+ if (!data.length) {
|
|
|
+ chartInstance.current.clear();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const names = data.map(d => d.name);
|
|
|
+ const values = data.map(d => d.value);
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ color,
|
|
|
+ title: { text: title, left: 'center' },
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ grid: horizontal
|
|
|
+ ? { left: 80, right: 50, top: 60, bottom: 60 }
|
|
|
+ : { left: 60, right: 40, top: 60, bottom: 100 },
|
|
|
+ xAxis: horizontal
|
|
|
+ ? { type: 'value' }
|
|
|
+ : {
|
|
|
+ type: 'category',
|
|
|
+ data: names,
|
|
|
+ axisLabel: {
|
|
|
+ interval: 0,
|
|
|
+ rotate: 30, // 防止长文本遮挡
|
|
|
+ overflow: 'truncate'
|
|
|
}
|
|
|
- })
|
|
|
- })
|
|
|
- myChart = init(ref.current)
|
|
|
- // 设置是否可缩放
|
|
|
- let dataZoom = []
|
|
|
- if (dataZoomInside) {
|
|
|
- dataZoom.push({
|
|
|
- type: 'inside'
|
|
|
- })
|
|
|
- }
|
|
|
- if (dataZoomSlider) {
|
|
|
- dataZoom.push({
|
|
|
- type: 'slider',
|
|
|
- width: 200,
|
|
|
- height: 20,
|
|
|
- top: 0,
|
|
|
- left: 140,
|
|
|
- disabled: dataZoomSlider
|
|
|
- })
|
|
|
- }
|
|
|
- setMyChart(myChart)
|
|
|
- myChart.setOption({
|
|
|
- /**鼠标悬浮 */
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis'
|
|
|
- },
|
|
|
- /**图列 */
|
|
|
- legend: {
|
|
|
- data: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }) => item.legendName),
|
|
|
- bottom: 0,
|
|
|
- left: 'center',
|
|
|
- show: data && data?.length > 1 ? true : false,//数据只有一条就隐藏
|
|
|
- ...textStyle
|
|
|
- },
|
|
|
- xAxis: {
|
|
|
- data: Xdata,
|
|
|
- boundaryGap: false,//贴边
|
|
|
- },
|
|
|
- yAxis: [
|
|
|
- {
|
|
|
- type: 'value',
|
|
|
- axisLabel: {//名称
|
|
|
- fontWeight: 900,
|
|
|
- fontSize: 15,
|
|
|
- color: '#333333',
|
|
|
- textBorderColor: '#dfdfdf',
|
|
|
- textBorderWidth: 1
|
|
|
- },
|
|
|
- },
|
|
|
- ],
|
|
|
- dataZoom,
|
|
|
- grid: {
|
|
|
- left: 100,
|
|
|
- right: 50,
|
|
|
},
|
|
|
- ...textStyle,
|
|
|
- /**工具保存为图片 */
|
|
|
- toolbox: {
|
|
|
- right: 50,
|
|
|
- // feature: {
|
|
|
- // saveAsImage: {
|
|
|
- // title: '保存为图片'
|
|
|
- // }
|
|
|
- // }
|
|
|
- },
|
|
|
- title: {
|
|
|
- text: title || '',
|
|
|
- left: '45%',
|
|
|
- top: 330,
|
|
|
- textStyle: {
|
|
|
- color: '#3946c3',
|
|
|
- fontSize: 16
|
|
|
- },
|
|
|
- },
|
|
|
- /**线图参数 */
|
|
|
- series: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }, index: number) => {
|
|
|
- return {
|
|
|
- name: item.legendName,//名称
|
|
|
- type: 'line',//图形
|
|
|
- data: Ydatas[index],//数据
|
|
|
- areaStyle: areaStyle ? {} : undefined,//是否开启块级颜色
|
|
|
- color: Array.isArray(color) ? color[index] : color,//颜色
|
|
|
- markPoint: {//最大值最小值
|
|
|
- data: markPoint ? markPoint : []
|
|
|
- },
|
|
|
- emphasis: series ? { focus: 'series' } : {}, //堆叠
|
|
|
- smooth
|
|
|
- }
|
|
|
- })
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [ref?.current, data])
|
|
|
- /**卸载myChart */
|
|
|
- useEffect(() => {
|
|
|
- if ((data as any[])?.length === 0) {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart?.id)
|
|
|
- }
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (myChart?.id) {
|
|
|
- dispose(myChart.id)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [myChart?.id, data])
|
|
|
- return <>
|
|
|
- {
|
|
|
- (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
|
|
|
+ yAxis: horizontal
|
|
|
+ ? { type: 'category', data: names }
|
|
|
+ : { type: 'value' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: values,
|
|
|
+ barWidth: '50%',
|
|
|
+ label: { show: true, position: horizontal ? 'right' : 'top' },
|
|
|
+ itemStyle: { borderRadius: 4 }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
|
|
|
- }
|
|
|
- {
|
|
|
- (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
- </div>
|
|
|
- }
|
|
|
- </>
|
|
|
-}
|
|
|
+ loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
|
|
|
+ chartInstance.current.setOption(option, true);
|
|
|
+ }, [data, title, color, horizontal, loading]);
|
|
|
|
|
|
-/**
|
|
|
- * 常用option配置//https://echarts.apache.org/next/zh/option.html#legend
|
|
|
- * @param legend 图例组件 用于用户操作筛选图表展示的组件
|
|
|
- * @param title 图标标题
|
|
|
- * @param grid 网格控制 可在一个渲染框架中渲染多个图表 或单个图表的间距宽高等值的设定
|
|
|
- * @param xAxis grid 中的 x 轴的设置
|
|
|
- * @param yAxis grid 中的 y 轴的设置
|
|
|
- * @param tooltip 鼠标悬浮的提示组件
|
|
|
- * @param series 系列数据配置
|
|
|
- */
|
|
|
-function useEcharts() {
|
|
|
- return {
|
|
|
- Echarts,
|
|
|
- Map,
|
|
|
- PieFocus,
|
|
|
- Line,
|
|
|
- Bar,
|
|
|
- BarMonitor,
|
|
|
- LineMonitor
|
|
|
- }
|
|
|
-}
|
|
|
-export default useEcharts
|
|
|
+ return <div ref={chartRef} style={style} />;
|
|
|
+};
|
|
|
+
|
|
|
+/* =====================================================
|
|
|
+ 导出 Hook
|
|
|
+===================================================== */
|
|
|
+const useEcharts = () => ({ Echarts, Line, Pie, Bar });
|
|
|
+export default useEcharts;
|