feat(web): components update

This commit is contained in:
zhaoying
2026-03-07 12:18:11 +08:00
parent 4c18f9e858
commit 0b3b241436
44 changed files with 1881 additions and 345 deletions

View File

@@ -0,0 +1,306 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-10 13:36:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-25 13:51:52
*/
/*
* AreaLineChart Component
*
* A reusable area line chart component built with ECharts that displays time-series data
* with gradient-filled areas under the lines. Supports multiple data series with
* customizable colors and responsive behavior.
*
* Features:
* - Multiple line series with gradient area fills
* - Gradient line colors (white to color to white)
* - Customizable x-axis key for flexible data structures
* - Date-based x-axis with formatted labels (DD/MM)
* - Responsive resizing using ResizeObserver
* - Interactive tooltips on hover
* - Customizable grid layout and colors
* - Legend at the bottom for series identification
* - Empty state when no data is available
* - Smooth rendering with requestAnimationFrame
*/
import { type FC, useEffect, useRef, useMemo } from 'react'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import { formatDateTime } from '@/utils/format';
import Empty from '@/components/Empty'
/** Base configuration for all line series */
const SeriesConfig = {
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 5,
showSymbol: true,
label: {
show: false,
position: 'top'
},
emphasis: {
focus: 'series'
},
}
/** Default color palette for area line series */
const Colors = ['#155EEF', '#FFB048', '#4DA8FF']
/**
* Data structure for chart data points
* Flexible structure allowing any string key with string or number values
*
* @interface ChartData
* @property {string | number} [key: string] - Dynamic properties for x-axis and data series
*/
export interface ChartData {
[key: string]: string | number;
}
/**
* Props for the AreaLineChart component
*
* @interface AreaLineChartProps
* @property {string} xAxisKey - Key name in chartData to use for x-axis values
* @property {ChartData[]} chartData - Array of data points with dynamic properties
* @property {Record<string, string>} seriesList - Map of data keys to display names
* @property {string} [className] - Additional CSS classes for the container
* @property {number} [height] - Height of the chart in pixels
* @property {string[]} [colors] - Custom color array for line series and gradients
* @property {any} [grid] - ECharts grid configuration for chart positioning
*/
interface AreaLineChartProps {
xAxisKey: string;
chartData: ChartData[];
seriesList: Record<string, string>;
className?: string;
height?: number;
colors?: string[];
grid?: any;
lineStyle?: any;
showLegend?: boolean;
smooth?: boolean;
}
/**
* AreaLineChart Component
*
* Renders a multi-series area line chart with gradient fills.
* The area gradient goes from the series color at the top to white at the bottom.
* The line gradient goes from white to the series color and back to white.
* Automatically resizes when container dimensions change.
*
* @param {AreaLineChartProps} props - Component props
* @returns {JSX.Element} Rendered area line chart or empty state
*
* @example
* ```tsx
* <AreaLineChart
* xAxisKey="date"
* chartData={[
* { date: '2024-01-01', revenue: 1000, profit: 200 },
* { date: '2024-01-02', revenue: 1500, profit: 300 }
* ]}
* seriesList={{ revenue: 'Revenue', profit: 'Profit' }}
* height={300}
* />
* ```
*/
const AreaLineChart: FC<AreaLineChartProps> = ({
xAxisKey,
chartData,
seriesList,
height,
colors = Colors,
grid = {
top: 7,
left: 4,
right: 16,
bottom: 32,
containLabel: true
},
lineStyle,
showLegend = true,
smooth = true
}) => {
/** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null);
/** Flag to prevent multiple simultaneous resize operations */
const resizeScheduledRef = useRef(false)
/**
* Generate series configuration for each data series with gradient effects
* Creates area fills with vertical gradients (color to white)
* and line colors with horizontal gradients (white to color to white)
*
* @returns {Array} Array of ECharts series configurations with gradient styles
*/
const getSeries = () => {
return Object.entries(seriesList).map(([key, name], index) => ({
...SeriesConfig,
name: name,
data: chartData.map(vo => vo[key as keyof ChartData]),
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colors[index]
},
{
offset: 1,
color: '#FFFFFF'
}
])
},
lineStyle: lineStyle || {
width: 3,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: '#FFFFFF'
},
{
offset: 0.8,
color: colors[index]
},
{
offset: 1,
color: '#FFFFFF'
}
])
},
smooth
}))
}
/**
* Memoized legend data to prevent unnecessary recalculations
* Formats series list for display in chart legend
*/
const formatSeriesList = useMemo(() => {
return Object.entries(seriesList).map(([_key, name]) => ({
...SeriesConfig,
name: name,
}))
}, [seriesList])
/**
* Set up responsive behavior using ResizeObserver
* Resizes chart when parent container dimensions change
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
return () => {
resizeObserver.disconnect()
}
}, [chartData])
return (
<div style={{ height: `${height}px` }}>
{chartData && chartData.length > 0
? <ReactEcharts
ref={chartRef}
option={{
color: colors,
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
},
label: {
show: false
}
},
},
legend: {
show: showLegend,
data: formatSeriesList,
textStyle: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 16,
},
itemGap: 8,
padding: 0,
itemWidth: 26,
itemHeight: 10,
bottom: 0,
lineStyle: {
width: 3,
},
},
grid: grid,
xAxis: {
type: 'category',
data: chartData.map(item => formatDateTime(item[xAxisKey], 'DD/MM')),
boundaryGap: false,
axisLabel: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 17,
},
axisLine: {
show: false,
lineStyle: {
color: '#EBEBEB',
}
},
splitLine: {
show: false,
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC',
align: 'right',
lineHeight: 17,
},
axisLine: {
lineStyle: {
color: '#EBEBEB',
}
},
},
series: getSeries()
}}
style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
: <Empty size={120} className="rb:h-full!" />
}
</div>
)
}
export default AreaLineChart

View File

@@ -0,0 +1,295 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-10 13:36:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-25 13:49:04
*/
/*
* BarChart Component
*
* A reusable area line chart component built with ECharts that displays time-series data
* with gradient-filled areas under the lines. Supports multiple data series with
* customizable colors and responsive behavior.
*
* Features:
* - Multiple line series with gradient area fills
* - Gradient line colors (white to color to white)
* - Customizable x-axis key for flexible data structures
* - Date-based x-axis with formatted labels (DD/MM)
* - Responsive resizing using ResizeObserver
* - Interactive tooltips on hover
* - Customizable grid layout and colors
* - Legend at the bottom for series identification
* - Empty state when no data is available
* - Smooth rendering with requestAnimationFrame
*/
import { type FC, useEffect, useRef, useMemo } from 'react'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import { formatDateTime } from '@/utils/format';
import Empty from '@/components/Empty'
/** Base configuration for all line series */
const SeriesConfig = {
type: 'bar',
stack: 'Total',
symbol: 'circle',
symbolSize: 5,
showSymbol: true,
label: {
show: false,
position: 'top'
},
emphasis: {
focus: 'series'
},
showBackground: true,
}
/** Default color palette for area line series */
const Colors = ['#155EEF', '#FFB048', '#4DA8FF']
/**
* Data structure for chart data points
* Flexible structure allowing any string key with string or number values
*
* @interface ChartData
* @property {string | number} [key: string] - Dynamic properties for x-axis and data series
*/
export interface ChartData {
[key: string]: string | number;
}
/**
* Props for the BarChart component
*
* @interface BarChartProps
* @property {string} xAxisKey - Key name in chartData to use for x-axis values
* @property {ChartData[]} chartData - Array of data points with dynamic properties
* @property {Record<string, string>} seriesList - Map of data keys to display names
* @property {string} [className] - Additional CSS classes for the container
* @property {number} [height] - Height of the chart in pixels
* @property {string[]} [colors] - Custom color array for line series and gradients
* @property {any} [grid] - ECharts grid configuration for chart positioning
*/
interface BarChartProps {
xAxisKey: string;
chartData: ChartData[];
seriesList: Record<string, string>;
className?: string;
height?: number;
colors?: string[];
grid?: any;
itemStyle?: any;
showLegend?: boolean;
showBackground?: boolean;
}
/**
* BarChart Component
*
* Renders a multi-series area line chart with gradient fills.
* The area gradient goes from the series color at the top to white at the bottom.
* The line gradient goes from white to the series color and back to white.
* Automatically resizes when container dimensions change.
*
* @param {BarChartProps} props - Component props
* @returns {JSX.Element} Rendered area line chart or empty state
*
* @example
* ```tsx
* <BarChart
* xAxisKey="date"
* chartData={[
* { date: '2024-01-01', revenue: 1000, profit: 200 },
* { date: '2024-01-02', revenue: 1500, profit: 300 }
* ]}
* seriesList={{ revenue: 'Revenue', profit: 'Profit' }}
* height={300}
* />
* ```
*/
const BarChart: FC<BarChartProps> = ({
xAxisKey,
chartData,
seriesList,
height,
colors = Colors,
grid = {
top: 7,
left: 4,
right: 16,
bottom: 32,
containLabel: true
},
itemStyle,
showLegend = true,
showBackground = true,
}) => {
/** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null);
/** Flag to prevent multiple simultaneous resize operations */
const resizeScheduledRef = useRef(false)
/**
* Generate series configuration for each data series with gradient effects
* Creates area fills with vertical gradients (color to white)
* and line colors with horizontal gradients (white to color to white)
*
* @returns {Array} Array of ECharts series configurations with gradient styles
*/
const getSeries = () => {
return Object.entries(seriesList).map(([key, name], index) => ({
...SeriesConfig,
name: name,
data: chartData.map(vo => vo[key as keyof ChartData]),
barWidth: 16,
itemStyle: itemStyle || {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colors[index]
},
{
offset: 1,
color: '#FFFFFF'
}
]),
},
emphasis: {
itemStyle: {
}
},
barGap: '-100%',
showBackground: showBackground,
}))
}
/**
* Memoized legend data to prevent unnecessary recalculations
* Formats series list for display in chart legend
*/
const formatSeriesList = useMemo(() => {
return Object.entries(seriesList).map(([_key, name]) => ({
...SeriesConfig,
name: name,
}))
}, [seriesList])
/**
* Set up responsive behavior using ResizeObserver
* Resizes chart when parent container dimensions change
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
return () => {
resizeObserver.disconnect()
}
}, [chartData])
return (
<div style={{ height: `${height}px` }}>
{chartData && chartData.length > 0
? <ReactEcharts
ref={chartRef}
option={{
color: colors,
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
},
label: {
show: false
}
},
},
legend: {
show: showLegend,
data: formatSeriesList,
textStyle: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 16,
},
itemGap: 8,
padding: 0,
itemWidth: 26,
itemHeight: 10,
bottom: 0,
itemStyle: {
width: 3,
},
},
grid: grid,
xAxis: {
type: 'category',
data: chartData.map(item => formatDateTime(item[xAxisKey], 'DD/MM')),
boundaryGap: false,
axisLabel: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 17,
},
axisLine: {
show: false,
itemStyle: {
color: '#EBEBEB',
}
},
splitLine: {
show: false,
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC',
align: 'right',
lineHeight: 17,
},
axisLine: {
itemStyle: {
color: '#EBEBEB',
}
},
},
series: getSeries()
}}
style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
: <Empty size={120} className="rb:h-full!" />
}
</div>
)
}
export default BarChart

View File

@@ -0,0 +1,200 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-10 14:06:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 14:06:09
*/
/**
* GraphNetworkChart Component
*
* A force-directed graph visualization component built with ECharts.
* Displays nodes and edges in an interactive network diagram with physics-based layout.
* Supports zooming, panning, dragging nodes, and click interactions.
*/
import { type FC, useEffect, useRef, type SetStateAction, type Dispatch } from 'react'
import ReactEcharts from 'echarts-for-react';
import PageEmpty from '@/components/Empty/PageEmpty'
// Default color palette for node categories
const Colors = ['#171719', '#155EEF', '#9C6FFF', '#FF8A4C']
/**
* Node interface representing a graph node/vertex
*/
export interface Node {
id: string; // Unique identifier for the node
label: string; // Display label for the node
category: number; // Category index for grouping and coloring
symbolSize: number; // Size of the node symbol in pixels
name: string; // Node name (used in ECharts)
itemStyle: {
color: string; // Custom color for this node
}
caption: string; // Additional description or caption
[key: string]: any; // Allow additional custom properties
}
/**
* Edge interface representing a connection between two nodes
*/
export interface Edge {
id: string; // Unique identifier for the edge
source: string; // Source node ID
target: string; // Target node ID
type: string; // Type/category of the relationship
caption: string; // Description of the relationship
value: number; // Numeric value associated with the edge
weight: number; // Weight/strength of the connection
}
/**
* Props for the GraphNetworkChart component
*/
interface GraphNetworkChartProps {
nodes: Node[]; // Array of nodes to display in the graph
links: Edge[]; // Array of edges connecting the nodes
categories: { name: string }[]; // Category definitions for node grouping
colors?: string[]; // Optional custom color palette (defaults to Colors)
onNodeClick: Dispatch<SetStateAction<Node | null>>; // Callback when a node is clicked
}
const GraphNetworkChart: FC<GraphNetworkChartProps> = ({
nodes,
links,
categories,
colors = Colors,
onNodeClick,
}) => {
// Reference to the ECharts instance for programmatic control
const chartRef = useRef<ReactEcharts>(null);
// Flag to prevent multiple simultaneous resize operations (debouncing)
const resizeScheduledRef = useRef(false)
/**
* Effect: Handle responsive chart resizing
*
* Uses ResizeObserver to detect container size changes and resize the chart accordingly.
* Implements requestAnimationFrame for smooth, debounced resize operations.
* Re-runs when nodes change to ensure proper sizing with new data.
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
// Use requestAnimationFrame for smooth, optimized resize
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
// Observe the chart container for size changes
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
// Cleanup: disconnect observer when component unmounts
return () => {
resizeObserver.disconnect()
}
}, [nodes])
return (
<div className="rb:w-full rb:h-full">
{/* Render chart only if nodes exist, otherwise show empty state */}
{nodes && nodes.length > 0
? <ReactEcharts
ref={chartRef}
option={{
// Color palette for node categories
colors: colors,
// Disable default tooltip (custom interaction via onNodeClick)
tooltip: {
show: false
},
// Hide legend (categories not displayed in legend)
legend: {
show: false,
bottom: 12,
},
series: [
{
type: 'graph', // Graph/network chart type
layout: 'force', // Force-directed layout algorithm
data: nodes || [], // Node data
links: links || [], // Edge data
categories: categories, // Category definitions
roam: true, // Enable zoom and pan interactions
// Dynamic zoom level based on node count for better initial view
zoom: nodes.length < 50 ? 3 : nodes.length < 100 ? 2 : 1,
// Node label configuration
label: {
show: true, // Display labels
position: 'right', // Position label to the right of node
formatter: '{b}', // Use node name as label text
},
// Edge styling
lineStyle: {
color: '#5B6167', // Gray color for edges
curveness: 0.3 // Slight curve for better visibility
},
// Force-directed layout physics configuration
force: {
repulsion: 100, // Repulsion force between nodes
edgeLength: 80, // Ideal distance between connected nodes
gravity: 0.3, // Gravity pulling nodes to center
layoutAnimation: true, // Animate layout changes
preventOverlap: true, // Prevent nodes from overlapping
edgeSymbol: ['none', 'arrow'], // Arrow on target end of edge
edgeSymbolSize: [4, 10], // Size of edge symbols
initLayout: 'force' // Use force-directed for initial layout
},
selectedMode: 'single', // Allow selecting one node at a time
draggable: true, // Enable dragging nodes
animationDurationUpdate: 0, // Disable animation on data update for performance
// Styling for selected nodes
select: {
itemStyle: {
borderWidth: 2, // Thicker border when selected
borderColor: '#ffffff', // White border for contrast
shadowBlur: 10, // Glow effect on selection
}
}
}
]
}}
style={{ height: '100%', width: '100%' }}
notMerge={false} // Merge options instead of replacing (better performance)
lazyUpdate={true} // Batch updates for better performance
// Event handlers
onEvents={{
click: (params: { dataType: string; data: Node; name: string }) => {
// Only trigger callback for node clicks (not edges or background)
if (params.dataType === 'node') {
onNodeClick(params.data)
}
}
}}
/>
: <PageEmpty />
}
</div>
)
}
export default GraphNetworkChart

View File

@@ -0,0 +1,260 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-10 13:35:55
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 13:35:55
*/
/*
* LineChart Component
*
* A reusable line chart component built with ECharts for displaying time-series data
* with multiple data series. Supports customizable colors, responsive behavior,
* and interactive tooltips.
*
* Features:
* - Multiple line series with different colors
* - Date-based x-axis with formatted labels (DD/MM)
* - Responsive resizing using ResizeObserver
* - Interactive tooltips on hover
* - Customizable grid layout and colors
* - Legend at the bottom for series identification
* - Empty state when no data is available
* - Smooth rendering with requestAnimationFrame
*/
import { type FC, useEffect, useRef, useMemo } from 'react'
import ReactEcharts from 'echarts-for-react';
import { formatDateTime } from '@/utils/format';
import Empty from '@/components/Empty'
/** Base configuration for all line series */
const SeriesConfig = {
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 5,
showSymbol: true,
label: {
show: false,
position: 'top'
},
emphasis: {
focus: 'series'
},
}
/** Default color palette for line series */
const Colors = ['#171719', '#155EEF', '#FF5D34']
/**
* Data structure for chart data points
*
* @interface ChartData
* @property {string | number} date - Date value for x-axis (timestamp or date string)
* @property {string | number} [key: string] - Dynamic properties for different data series
*/
export interface ChartData {
date: string | number;
[key: string]: string | number;
}
/**
* Props for the LineChart component
*
* @interface LineChartProps
* @property {ChartData[]} chartData - Array of data points with date and series values
* @property {Record<string, string>} seriesList - Map of data keys to display names
* @property {string} [className] - Additional CSS classes for the container
* @property {number} [height] - Height of the chart in pixels
* @property {string[]} [colors] - Custom color array for line series
* @property {any} [grid] - ECharts grid configuration for chart positioning
*/
interface LineChartProps {
chartData: ChartData[];
seriesList: Record<string, string>;
className?: string;
height?: number;
colors?: string[];
grid?: any;
}
/**
* LineChart Component
*
* Renders a multi-series line chart with date-based x-axis.
* Automatically resizes when container dimensions change.
*
* @param {LineChartProps} props - Component props
* @returns {JSX.Element} Rendered line chart or empty state
*
* @example
* ```tsx
* <LineChart
* chartData={[
* { date: '2024-01-01', users: 100, sessions: 200 },
* { date: '2024-01-02', users: 150, sessions: 250 }
* ]}
* seriesList={{ users: 'Active Users', sessions: 'Sessions' }}
* height={300}
* />
* ```
*/
const LineChart: FC<LineChartProps> = ({
chartData,
seriesList,
height,
colors = Colors,
grid = {
top: 7,
right: 16,
}
}) => {
/** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null);
/** Flag to prevent multiple simultaneous resize operations */
const resizeScheduledRef = useRef(false)
/**
* Generate series configuration for each data series
* Maps seriesList keys to chart series with corresponding data and colors
*
* @returns {Array} Array of ECharts series configurations
*/
const getSeries = () => {
return Object.entries(seriesList).map(([key, name], index) => ({
...SeriesConfig,
name: name,
data: chartData.map(vo => vo[key as keyof ChartData]),
lineStyle: {
width: 2,
color: colors[index]
},
}))
}
/**
* Memoized legend data to prevent unnecessary recalculations
* Formats series list for display in chart legend
*/
const formatSeriesList = useMemo(() => {
return Object.entries(seriesList).map(([_key, name]) => ({
...SeriesConfig,
name: name,
}))
}, [seriesList])
/**
* Set up responsive behavior using ResizeObserver
* Resizes chart when parent container dimensions change
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
return () => {
resizeObserver.disconnect()
}
}, [chartData])
return (
<div style={{ height: `${height}px` }}>
{chartData && chartData.length > 0
? <ReactEcharts
ref={chartRef}
option={{
color: colors,
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
},
label: {
show: false
}
},
},
legend: {
data: formatSeriesList,
textStyle: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 16,
},
itemGap: 8,
padding: 0,
itemWidth: 26,
itemHeight: 10,
bottom: 0,
lineStyle: {
width: 3,
},
},
grid: grid,
xAxis: {
type: 'category',
data: chartData.map(item => formatDateTime(item.date, 'DD/MM')),
boundaryGap: false,
axisLabel: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 17,
},
axisLine: {
show: false,
lineStyle: {
color: '#EBEBEB',
}
},
splitLine: {
show: false,
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC',
align: 'right',
lineHeight: 17,
},
axisLine: {
lineStyle: {
color: '#EBEBEB',
}
},
},
series: getSeries()
}}
style={{ height: '100%', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
: <Empty size={120} className="rb:h-full!" />
}
</div>
)
}
export default LineChart

View File

@@ -0,0 +1,193 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-10 13:35:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 13:35:45
*/
/*
* PieChart Component
*
* A reusable pie chart component built with ECharts that displays data distribution
* in a donut chart format with customizable colors and responsive behavior.
*
* Features:
* - Donut-style pie chart with percentage labels
* - Customizable color palette
* - Responsive resizing using ResizeObserver
* - Hover tooltips showing percentage values
* - Legend at the bottom with horizontal layout
* - Empty state when no data is available
* - Shadow effects for better visual depth
*/
import { type FC, useEffect, useRef } from 'react'
import ReactEcharts from 'echarts-for-react';
import Empty from '@/components/Empty'
/** Default color palette for pie chart segments */
const Colors = ['#171719', '#155EEF', '#4DA8FF', '#9C6FFF', '#ABEBFF', '#DFE4ED']
/**
* Data structure for each pie chart segment
*
* @interface ChartData
* @property {string} name - Label for the segment (displayed in legend)
* @property {number} value - Numeric value for the segment (determines size)
*/
export interface ChartData {
name: string;
value: number;
}
/**
* Props for the PieChart component
*
* @interface PieChartProps
* @property {ChartData[]} chartData - Array of data points to display in the chart
* @property {number} [height=260] - Height of the chart in pixels
* @property {string[]} [colors] - Custom color array for chart segments (defaults to Colors)
*/
interface PieChartProps {
chartData: ChartData[];
height?: number;
colors?: string[];
}
/**
* PieChart Component
*
* Renders a donut-style pie chart with percentage labels and legend.
* Automatically resizes when container dimensions change.
*
* @param {PieChartProps} props - Component props
* @returns {JSX.Element} Rendered pie chart or empty state
*
* @example
* ```tsx
* <PieChart
* chartData={[
* { name: 'Category A', value: 30 },
* { name: 'Category B', value: 70 }
* ]}
* height={300}
* />
* ```
*/
const PieChart: FC<PieChartProps> = ({
chartData,
height = 260,
colors = Colors,
}) => {
/** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null);
/** Flag to prevent multiple simultaneous resize operations */
const resizeScheduledRef = useRef(false)
/**
* Set up responsive behavior using ResizeObserver
* Resizes chart when parent container dimensions change
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
// Use requestAnimationFrame for smooth resize performance
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
// Cleanup: disconnect observer when component unmounts
return () => {
resizeObserver.disconnect()
}
}, [chartData])
return (
<div style={{ height: `${height}px` }}>
{chartData && chartData.length > 0
? <ReactEcharts
ref={chartRef}
option={{
color: colors,
tooltip: {
trigger: 'item',
textStyle: {
color: '#5B6167',
fontSize: 12,
width: 27,
height: 16,
},
formatter: '{d}%',
padding: [8, 5],
backgroundColor: '#FFFFFF',
borderColor: '#DFE4ED',
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
},
legend: {
bottom: 0,
padding: 0,
itemWidth: 12,
itemHeight: 12,
borderRadius: 2,
orient: 'horizontal',
itemGap: 48,
textStyle: {
color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 16,
}
},
series: [
{
type: 'pie',
radius: ['60%', '100%'],
avoidLabelOverlap: false,
percentPrecision: 0,
padAngle: 1,
width: 182,
height: 182,
left: 'center',
top: 24,
itemStyle: {
borderRadius: 2,
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 2,
shadowColor: 'rgba(0,0,0,0.25)',
},
label: {
fontWeight: 'bold',
color: '#171719',
formatter: '{d}%',
fontFamily: 'MiSans-Demibold',
},
labelLine: {
lineStyle: {
color: '#DFE4ED'
}
},
data: chartData
}
]
}}
style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
: <Empty size={120} className="rb:h-full!" />
}
</div>
)
}
export default PieChart