4.3 集成可视化库:从 2D 图表到 3D 数据星系

数据如果只是干巴巴的数字,很难让人直观地发现规律。作为数据分析平台,高质量的数据可视化是提升用户体验(UX)的核心手段。

在现代 Web 前端生态中,数据可视化方案百花齐放。本节我们将打破局限,在项目中集成业界最顶级的 2D 图表库(Apache ECharts) 以及炫酷的 3D 渲染库(Three.js 及其 React 封装),为我们的 AI Analyzer 注入强大的视觉表现力。

源码地址:https://github.com/you-want/ai-data-analyzer

欢迎 star,支持一波。


1. 为什么选择 ECharts 和 Three.js?

对于企业级和需要极强视觉冲击力的产品而言,我们需要重型的武器:

  1. Apache ECharts:百度开源、现已捐赠给 Apache 基金会。它是目前国内乃至全球功能最丰富、性能最强悍(支持 Canvas 和 SVG 双引擎,轻松处理十万级数据)的 2D 图表库。
  2. Three.js & React Three Fiber (R3F):Three.js 是 WebGL 的标准。而 R3F 是一个极其优秀的 React 封装,允许我们用声明式的组件语法来写 3D 场景。我们将用它来构建一个炫酷的"3D 数据星系"。

安装依赖

进入 frontend 目录,安装这些强大的可视化工具:

bash 复制代码
cd frontend
# 安装 ECharts 相关
pnpm add echarts echarts-for-react
# 安装 3D 渲染相关
pnpm add three @react-three/fiber @react-three/drei
pnpm add -D @types/three

2. 构建高级 2D 销售趋势图 (ECharts)

ECharts 的配置项 (Option) 非常强大。我们创建一个支持渐变色、双折线、堆叠以及自适应暗黑模式的复杂图表组件。

新建文件 frontend/src/components/charts/AdvancedSalesChart.tsx

tsx 复制代码
"use client";

import React, { useMemo } from 'react';
import ReactECharts from 'echarts-for-react';

interface EChartsComponentProps {
  title?: string;
  data?: Record<string, unknown>[]; // 后续对接真实数据
}

export default function AdvancedSalesChart({ title = '全渠道销售趋势与预测' }: EChartsComponentProps) {
  // 模拟数据
  const xAxisData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月'];
  const onlineSales = [120, 132, 101, 134, 90, 230, 210, 250, 300];
  const offlineSales = [220, 182, 191, 234, 290, 330, 310, 280, 250];
  
  // 这里简化处理,实际项目可从 next-themes 获取 isDark 状态
  const isDark = false; 

  const option = useMemo(() => ({
    backgroundColor: 'transparent',
    tooltip: { trigger: 'axis' },
    legend: { data: ['线上销售', '线下销售'] },
    grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
    xAxis: [{
      type: 'category',
      boundaryGap: false,
      data: xAxisData,
    }],
    yAxis: [{ type: 'value' }],
    series: [
      {
        name: '线上销售',
        type: 'line',
        smooth: true,
        showSymbol: false,
        areaStyle: {
          opacity: 0.8,
          color: {
            type: 'linear',
            x: 0, y: 0, x2: 0, y2: 1,
            colorStops: [
              { offset: 0, color: 'rgba(59, 130, 246, 0.8)' },
              { offset: 1, color: 'rgba(59, 130, 246, 0.1)' }
            ]
          }
        },
        data: onlineSales
      },
      // ... 线下销售配置类似,颜色为绿色 (rgba(16, 185, 129))
    ]
  }), [isDark]);

  return (
    <div className="bg-white dark:bg-zinc-900 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-zinc-800 flex flex-col h-full">
      <h3 className="text-lg font-semibold text-gray-800 dark:text-zinc-100 mb-4">{title}</h3>
      <div className="flex-1 min-h-[300px]">
        <ReactECharts option={option} style={{ height: '100%', width: '100%' }} />
      </div>
    </div>
  );
}

提示: 我们使用了 useMemo 来缓存 option 配置,避免 React 重渲染时造成 ECharts 实例不必要的重绘。


3. 降维打击:构建 3D 数据星系 (Three.js)

传统的 2D 柱状图太普通了。在 AI Data Agent 平台中,为了向客户展示我们深厚的技术实力,我们引入一个由 Three.js 驱动的 3D 交互式数据图表。

新建文件 frontend/src/components/charts/DataGalaxy3D.tsx

tsx 复制代码
"use client";

import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Text, Float } from '@react-three/drei';
import * as THREE from 'three';

// 单个 3D 交互柱子组件
function Bar({ position, height, color, label }: any) {
  const meshRef = useRef<THREE.Mesh>(null);
  const [hovered, setHover] = useState(false);
  
  // 使用 useFrame 实现平滑的悬浮放大动画 (类似 requestAnimationFrame)
  useFrame(() => {
    if (meshRef.current) {
      const targetScaleY = hovered ? 1.1 : 1;
      meshRef.current.scale.y = THREE.MathUtils.lerp(meshRef.current.scale.y, targetScaleY, 0.1);
    }
  });

  return (
    <group position={position}>
      {/* 3D 柱子本体 */}
      <mesh 
        ref={meshRef} 
        position={[0, height / 2, 0]} 
        onPointerOver={() => setHover(true)}
        onPointerOut={() => setHover(false)}
        castShadow receiveShadow
      >
        <boxGeometry args={[0.8, height, 0.8]} />
        <meshStandardMaterial color={hovered ? '#ffffff' : color} roughness={0.2} metalness={0.8} />
      </mesh>
      
      {/* 底部 3D 文字标签 */}
      <Text position={[0, -0.5, 0.6]} rotation={[-Math.PI / 2, 0, 0]} fontSize={0.4} color="#888888" anchorX="center">
        {label}
      </Text>
      
      {/* 悬浮时显示的顶部浮动数值 */}
      {hovered && (
        <Float speed={5} rotationIntensity={0} floatIntensity={0.5}>
          <Text position={[0, height + 0.8, 0]} fontSize={0.5} color="#ffffff" anchorX="center">
            {height.toFixed(1)}
          </Text>
        </Float>
      )}
    </group>
  );
}

export default function DataGalaxy3D() {
  const data = [
    { label: 'Q1', value: 4.2, color: '#3B82F6' },
    { label: 'Q2', value: 6.8, color: '#10B981' },
    { label: 'Q3', value: 3.5, color: '#F59E0B' },
    { label: 'Q4', value: 8.4, color: '#EF4444' },
  ];

  return (
    <div className="bg-[#18181B] p-6 rounded-2xl shadow-sm border border-zinc-800 flex flex-col h-full relative overflow-hidden">
      <div className="absolute top-6 left-6 z-10">
        <h3 className="text-lg font-semibold text-zinc-100">数据星系 (3D 视图)</h3>
        <p className="text-xs text-zinc-400 mt-1">拖拽以旋转视角,悬浮查看数据</p>
      </div>
      
      {/* Canvas 是 Three.js 的渲染入口 */}
      <div className="flex-1 min-h-[300px] w-full h-full cursor-grab active:cursor-grabbing">
        <Canvas shadows camera={{ position: [0, 5, 12], fov: 45 }}>
          <color attach="background" args={['#18181B']} />
          <ambientLight intensity={0.5} />
          <directionalLight position={[10, 10, 5]} intensity={1} castShadow />
          
          <group position={[-3, -2, 0]}>
            {data.map((item, index) => (
              <Bar key={item.label} position={[index * 2, 0, 0]} height={item.value} color={item.color} label={item.label} />
            ))}
            {/* 底座 */}
            <mesh position={[3, -0.2, 0]} receiveShadow>
              <boxGeometry args={[9, 0.4, 3]} />
              <meshStandardMaterial color="#27272A" />
            </mesh>
          </group>

          {/* 允许用户拖拽旋转视角的控制器 */}
          <OrbitControls enablePan={false} minPolarAngle={Math.PI / 4} maxPolarAngle={Math.PI / 2.2} />
        </Canvas>
      </div>
    </div>
  );
}

4. 将图表集成到 Dashboard

最后,我们回到上一节编写的 frontend/src/app/dashboard/page.tsx,将这两个组件放置在指标卡片下方。

tsx 复制代码
// frontend/src/app/dashboard/page.tsx
import AdvancedSalesChart from '@/components/charts/AdvancedSalesChart';
import DataGalaxy3D from '@/components/charts/DataGalaxy3D';

// ... (其他引入保持不变)

export default async function DashboardPage() {
  return (
    <div className="space-y-8">
      {/* 1. 顶部指标卡片区域... */}
      
      {/* 2. 数据可视化区域:2D ECharts 与 3D Three.js */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 h-[450px]">
        {/* 左侧:2D 复杂图表 (ECharts) */}
        <AdvancedSalesChart />

        {/* 右侧:3D 数据星系 (Three.js / React Three Fiber) */}
        <DataGalaxy3D />
      </div>

      {/* 3. 业务功能操作区 (上传、任务监听与 AI 对话)... */}
    </div>
  );
}

现在打开你的浏览器,你会看到一个极具专业感和未来感的 Dashboard!左侧是平滑渐变的 ECharts 折线图,右侧是可以通过鼠标拖拽旋转、悬浮高亮互动的 3D 数据柱状图。你的 AI Agent 数据中台在视觉上已经遥遥领先了!