可视化数据实现(四)

简介: 教程来源 https://bncne.cn/sheyingjiqiao.html 本文系统讲解数据可视化核心技术:基于Spring Boot的后端API设计、Three.js 3D可视化、动态叙事引导、异常检测与地理信息展示,并通过组件化封装实现可复用图表库,强调以洞察为目标的技术实践。

第八部分:后端集成

8.1 数据API设计

// 数据API控制器
@RestController
@RequestMapping("/api/data")
public class DataApiController {

    @Autowired
    private DataService dataService;

    // 分页查询
    @GetMapping("/{dataset}")
    public PageResult getData(
            @PathVariable String dataset,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "100") int size,
            @RequestParam(required = false) String sortBy,
            @RequestParam(required = false) String sortDir,
            @RequestParam(required = false) String filter) {

        return dataService.query(dataset, page, size, sortBy, sortDir, filter);
    }

    // 聚合查询
    @GetMapping("/{dataset}/aggregate")
    public AggregationResult aggregate(
            @PathVariable String dataset,
            @RequestParam String groupBy,
            @RequestParam String metric,
            @RequestParam String aggregation,  // sum, avg, count, min, max
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {

        return dataService.aggregate(dataset, groupBy, metric, aggregation, startDate, endDate);
    }

    // 导出CSV
    @GetMapping("/{dataset}/export")
    public void exportData(
            @PathVariable String dataset,
            HttpServletResponse response) throws IOException {

        response.setContentType("text/csv");
        response.setHeader("Content-Disposition", "attachment; filename=data.csv");

        dataService.exportToCsv(dataset, response.getWriter());
    }
}

第九部分:3D可视化

9.1 Three.js 3D可视化
Three.js是最流行的WebGL库,可以在浏览器中创建复杂的3D可视化效果。3D可视化适合展示三维空间数据、复杂网络关系、地理地形等。

<!DOCTYPE html>
<html>
<head>
    <title>3D数据可视化 - Three.js</title>
    <style>
        body { margin: 0; overflow: hidden; }
        #info {
            position: absolute;
            top: 20px;
            left: 20px;
            color: white;
            background: rgba(0,0,0,0.7);
            padding: 10px;
            border-radius: 5px;
            font-family: Arial;
            z-index: 100;
            pointer-events: none;
        }
        #controls {
            position: absolute;
            bottom: 20px;
            left: 20px;
            color: white;
            background: rgba(0,0,0,0.7);
            padding: 10px;
            border-radius: 5px;
            font-family: Arial;
            z-index: 100;
        }
        button {
            margin: 5px;
            padding: 5px 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div id="info">
        <h2>3D数据可视化 - 销售数据分布</h2>
        <p>鼠标拖拽旋转视角 | 右键平移 | 滚轮缩放</p>
    </div>
    <div id="controls">
        <button id="toggle-grid">切换网格</button>
        <button id="toggle-labels">切换标签</button>
        <button id="reset-view">重置视角</button>
    </div>

    <!-- 引入Three.js核心库及扩展 -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.128.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';

        // --- 初始化场景、相机、渲染器 ---
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x050b1a);  // 深邃星空蓝黑背景
        scene.fog = new THREE.FogExp2(0x050b1a, 0.0008);  // 雾效增强景深

        // 透视相机 (视角, 宽高比, 近平面, 远平面)
        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);
        camera.position.set(15, 12, 18);
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true;  // 开启阴影映射
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;  // 柔和阴影
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);

        // CSS2渲染文字标签
        const labelRenderer = new CSS2DRenderer();
        labelRenderer.setSize(window.innerWidth, window.innerHeight);
        labelRenderer.domElement.style.position = 'absolute';
        labelRenderer.domElement.style.top = '0px';
        labelRenderer.domElement.style.left = '0px';
        labelRenderer.domElement.style.pointerEvents = 'none';
        document.body.appendChild(labelRenderer.domElement);

        // --- 轨道控制 (支持交互) ---
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;          // 惯性效果
        controls.dampingFactor = 0.05;
        controls.rotateSpeed = 1.0;
        controls.zoomSpeed = 1.2;
        controls.panSpeed = 0.8;
        controls.enableZoom = true;
        controls.enablePan = true;
        controls.target.set(0, 0, 0);

        // --- 辅助元素: 网格地面和坐标轴 (增强空间感) ---
        // 网格辅助平面
        const gridHelper = new THREE.GridHelper(30, 20, 0x88aaff, 0x335588);
        gridHelper.position.y = -3;
        gridHelper.material.transparent = true;
        gridHelper.material.opacity = 0.6;
        scene.add(gridHelper);

        // 简单的地面反射光晕 (一个半透明平面)
        const groundPlane = new THREE.Mesh(
            new THREE.PlaneGeometry(25, 25),
            new THREE.MeshStandardMaterial({ color: 0x112233, roughness: 0.5, metalness: 0.1, transparent: true, opacity: 0.3 })
        );
        groundPlane.rotation.x = -Math.PI / 2;
        groundPlane.position.y = -3.1;
        groundPlane.receiveShadow = true;
        scene.add(groundPlane);

        // 简易坐标轴 (红X, 绿Z)
        const axesHelper = new THREE.AxesHelper(8);
        axesHelper.material.transparent = true;
        axesHelper.material.opacity = 0.25;
        scene.add(axesHelper);

        // --- 灯光系统 (营造立体感和氛围) ---
        // 环境光
        const ambientLight = new THREE.AmbientLight(0x404060);
        scene.add(ambientLight);

        // 主光源方向光
        const dirLight = new THREE.DirectionalLight(0xffffff, 1);
        dirLight.position.set(5, 12, 8);
        dirLight.castShadow = true;
        dirLight.receiveShadow = false;
        dirLight.shadow.mapSize.width = 1024;
        dirLight.shadow.mapSize.height = 1024;
        scene.add(dirLight);

        // 补光 - 背光暖色
        const backLight = new THREE.PointLight(0xffaa66, 0.5);
        backLight.position.set(-3, 5, -5);
        scene.add(backLight);

        // 冷色侧补光
        const fillLight = new THREE.PointLight(0x6688ff, 0.4);
        fillLight.position.set(4, 3, 4);
        scene.add(fillLight);

        // 动态底部光晕 (点光源跟随相机方向轻微变化,这里简单加一个半球光)
        const hemiLight = new THREE.HemisphereLight(0x88aaff, 0x332222, 0.5);
        scene.add(hemiLight);

        // --- 核心: 生成模拟数据 (产品销售数据, 三维: X-区域, Y-销售额, Z-品类) ---
        // 数据点结构: { x, y, z, value, category, region, name }
        const categories = ['电子产品', '服装', '食品', '家居', '美妆'];
        const regions = ['华东', '华南', '华北', '西部', '海外'];

        // 颜色映射 (按品类)
        const colorMap = {
            '电子产品': 0x3b82f6,  // 蓝色
            '服装': 0xef4444,      // 红色
            '食品': 0x10b981,      // 绿色
            '家居': 0xf59e0b,      // 橙色
            '美妆': 0xec4899       // 粉色
        };

        // 存储柱状体Mesh和标签对象
        const bars = [];
        const labels = [];

        // 生成随机但有一定规律的数据点 (X:区域索引, Z:品类索引, Y:销售额)
        const dataPoints = [];
        for (let i = 0; i < regions.length; i++) {
            for (let j = 0; j < categories.length; j++) {
                // 模拟销售额基数 + 随机波动 + 品类/区域特色
                let baseSales = 5000 + Math.random() * 8000;
                // 电子产品在华东更高
                if (categories[j] === '电子产品' && regions[i] === '华东') baseSales *= 1.5;
                // 服装在华南更高
                if (categories[j] === '服装' && regions[i] === '华南') baseSales *= 1.4;
                // 食品在西部更高
                if (categories[j] === '食品' && regions[i] === '西部') baseSales *= 1.3;

                const sales = Math.floor(baseSales);
                const x = (i - (regions.length-1)/2) * 2.2;   // X轴间隔2.2
                const z = (j - (categories.length-1)/2) * 2.2; // Z轴间隔2.2
                const y = sales / 1800;  // 高度缩放系数,使最高柱约6单位

                dataPoints.push({
                    x, y, z,
                    sales: sales,
                    category: categories[j],
                    region: regions[i],
                    color: colorMap[categories[j]]
                });
            }
        }

        // 创建柱状体群组
        const barsGroup = new THREE.Group();

        dataPoints.forEach(point => {
            // 几何体: 长方体 (宽0.8, 高point.y, 深0.8)
            const geometry = new THREE.BoxGeometry(0.8, point.y, 0.8);
            const material = new THREE.MeshStandardMaterial({
                color: point.color,
                roughness: 0.3,
                metalness: 0.6,
                emissive: 0x000000,
                emissiveIntensity: 0
            });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.set(point.x, point.y / 2 - 3, point.z);  // 地面y=-3,所以中心在y/2-3
            cube.castShadow = true;
            cube.receiveShadow = false;
            cube.userData = { sales: point.sales, category: point.category, region: point.region };
            barsGroup.add(cube);

            // 添加边框线框,增强科技感
            const edgesGeo = new THREE.EdgesGeometry(geometry);
            const edgesMat = new THREE.LineBasicMaterial({ color: 0xffffff });
            const wireframe = new THREE.LineSegments(edgesGeo, edgesMat);
            cube.add(wireframe);

            // CSS2D文字标签 (显示销售额)
            const div = document.createElement('div');
            div.textContent = `${point.sales.toLocaleString()}万`;
            div.style.color = '#ffdd99';
            div.style.fontSize = '14px';
            div.style.fontWeight = 'bold';
            div.style.textShadow = '1px 1px 0px black';
            div.style.background = 'rgba(0,0,0,0.6)';
            div.style.padding = '2px 6px';
            div.style.borderRadius = '12px';
            div.style.border = '1px solid rgba(255,255,255,0.3)';
            div.style.whiteSpace = 'nowrap';

            const label = new CSS2DObject(div);
            label.position.set(point.x, point.y - 2.5, point.z);
            barsGroup.add(label);
            labels.push(label);
        });

        scene.add(barsGroup);

        // --- 添加区域和品类的辅助装饰线框 (增强空间理解) ---
        // 在X轴方向添加区域分隔线
        const xPositions = [-4.4, -2.2, 0, 2.2, 4.4];
        regions.forEach((region, idx) => {
            const x = xPositions[idx];
            // 垂直半透明柱状标识
            const pillarMat = new THREE.MeshPhongMaterial({ color: 0x88aaff, transparent: true, opacity: 0.15 });
            const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.2, 6, 9), pillarMat);
            pillar.position.set(x, 0, 0);
            scene.add(pillar);

            // 添加区域文字标签 (CSS2D)
            const div = document.createElement('div');
            div.textContent = region;
            div.style.color = '#aaddff';
            div.style.fontSize = '16px';
            div.style.fontWeight = 'bold';
            div.style.background = 'rgba(0,0,0,0.5)';
            div.style.padding = '4px 12px';
            div.style.borderRadius = '20px';
            div.style.border = '1px solid #88aaff';
            const label = new CSS2DObject(div);
            label.position.set(x, -2, -5.5);
            scene.add(label);
        });

        // 在Z轴方向添加品类文字标签
        const zPositions = [-4.4, -2.2, 0, 2.2, 4.4];
        categories.forEach((cat, idx) => {
            const z = zPositions[idx];
            const div = document.createElement('div');
            div.textContent = cat;
            div.style.color = '#ffcc88';
            div.style.fontSize = '16px';
            div.style.fontWeight = 'bold';
            div.style.background = 'rgba(0,0,0,0.5)';
            div.style.padding = '4px 12px';
            div.style.borderRadius = '20px';
            div.style.border = '1px solid #ffaa66';
            const label = new CSS2DObject(div);
            label.position.set(-6, -2, z);
            scene.add(label);
        });

        // --- 添加粒子系统: 数据光点 (营造科技感氛围) ---
        const particleCount = 1500;
        const particlesGeometry = new THREE.BufferGeometry();
        const particlePositions = new Float32Array(particleCount * 3);
        for (let i = 0; i < particleCount; i++) {
            // 分布在柱状区域周围
            particlePositions[i*3] = (Math.random() - 0.5) * 22;
            particlePositions[i*3+1] = (Math.random() - 0.5) * 12 - 2;
            particlePositions[i*3+2] = (Math.random() - 0.5) * 18;
        }
        particlesGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
        const particlesMaterial = new THREE.PointsMaterial({
            color: 0x88aaff,
            size: 0.08,
            transparent: true,
            opacity: 0.6,
            blending: THREE.AdditiveBlending
        });
        const particles = new THREE.Points(particlesGeometry, particlesMaterial);
        scene.add(particles);

        // 缓慢旋转粒子系统
        function animateParticles() {
            particles.rotation.y += 0.0005;
            particles.rotation.x += 0.0003;
            requestAnimationFrame(animateParticles);
        }
        animateParticles();

        // --- 动画: 让一些光点沿柱状体上下浮动 (额外效果) ---
        const floatingLights = [];
        for (let i = 0; i < 60; i++) {
            const lightGeo = new THREE.SphereGeometry(0.08, 8, 8);
            const lightMat = new THREE.MeshStandardMaterial({ color: 0xffaa66, emissive: 0xff4400, emissiveIntensity: 0.8 });
            const lightSphere = new THREE.Mesh(lightGeo, lightMat);
            // 随机绑定到一个柱体上
            const randomBar = barsGroup.children.find(child => child.isMesh && child.geometry.type === 'BoxGeometry');
            if (randomBar) {
                lightSphere.userData = { parentBar: randomBar, offsetY: Math.random() * randomBar.geometry.parameters.height };
                lightSphere.position.copy(randomBar.position);
                lightSphere.position.y += lightSphere.userData.offsetY - 3;
                scene.add(lightSphere);
                floatingLights.push(lightSphere);
            }
        }

        // 浮动动画
        function animateFloatingLights() {
            floatingLights.forEach(light => {
                if (light.userData && light.userData.parentBar) {
                    const bar = light.userData.parentBar;
                    const maxY = bar.position.y + bar.geometry.parameters.height/2;
                    let yOffset = light.userData.offsetY;
                    yOffset += 0.01;
                    if (yOffset > bar.geometry.parameters.height) yOffset = 0;
                    light.userData.offsetY = yOffset;
                    light.position.y = bar.position.y - bar.geometry.parameters.height/2 + yOffset;
                }
            });
            requestAnimationFrame(animateFloatingLights);
        }
        animateFloatingLights();

        // --- 辅助功能: 高亮当前悬停的柱体 (射线检测) ---
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();

        function onMouseMove(event) {
            // 计算鼠标位置归一化坐标
            mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
            mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1;

            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(barsGroup.children, true);

            // 重置所有材质高亮
            barsGroup.children.forEach(child => {
                if (child.isMesh && child.material) {
                    child.material.emissiveIntensity = 0;
                    child.material.color.setHex(child.userData.originalColor || child.material.color.getHex());
                }
            });

            if (intersects.length > 0) {
                const hit = intersects[0].object;
                if (hit.isMesh) {
                    // 保存原始颜色
                    if (!hit.userData.originalColor) hit.userData.originalColor = hit.material.color.getHex();
                    hit.material.emissiveIntensity = 0.5;
                    hit.material.color.setHex(0xffaa66);

                    // 显示详细信息工具提示 (简单实现)
                    const infoDiv = document.getElementById('info');
                    if (infoDiv && hit.userData) {
                        const salesVal = hit.userData.sales || '?';
                        const categoryVal = hit.userData.category || '?';
                        const regionVal = hit.userData.region || '?';
                        infoDiv.innerHTML = `<h2>3D数据可视化 - 销售数据分布</h2>
                                             <p><strong>区域:</strong> ${regionVal} &nbsp;|&nbsp; <strong>品类:</strong> ${categoryVal}</p>
                                             <p><strong>销售额:</strong> ${salesVal.toLocaleString()} 万元</p>
                                             <p>鼠标拖拽旋转视角 | 右键平移 | 滚轮缩放</p>`;
                    }
                }
            } else {
                // 恢复默认信息
                const infoDiv = document.getElementById('info');
                if (infoDiv) {
                    infoDiv.innerHTML = `<h2>3D数据可视化 - 销售数据分布</h2>
                                         <p>鼠标拖拽旋转视角 | 右键平移 | 滚轮缩放</p>`;
                }
            }
        }

        window.addEventListener('mousemove', onMouseMove, false);

        // --- 控制按钮功能 ---
        document.getElementById('toggle-grid').addEventListener('click', () => {
            gridHelper.visible = !gridHelper.visible;
            groundPlane.visible = !groundPlane.visible;
        });
        document.getElementById('toggle-labels').addEventListener('click', () => {
            labels.forEach(label => { label.visible = !label.visible; });
        });
        document.getElementById('reset-view').addEventListener('click', () => {
            camera.position.set(15, 12, 18);
            controls.target.set(0, 0, 0);
            controls.update();
        });

        // --- 动画循环 ---
        function animate() {
            requestAnimationFrame(animate);
            controls.update();  // 更新轨道控制
            renderer.render(scene, camera);
            labelRenderer.render(scene, camera);
        }
        animate();

        // 窗口适配
        window.addEventListener('resize', onWindowResize, false);
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
            labelRenderer.setSize(window.innerWidth, window.innerHeight);
        }

        console.log('3D可视化已启动,支持交互探索');
    </script>
</body>
</html>

9.2 3D散点图与气泡图

// 使用Three.js创建3D散点图(适合展示三维数据关系)
class Scatter3D {
    constructor(containerId, data, options) {
        this.container = document.getElementById(containerId);
        this.data = data;
        this.options = options;
        this.scene = null;
        this.camera = null;
        this.renderer = null;
        this.points = [];

        this.init();
    }

    init() {
        // 场景
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(0x0a0a2a);

        // 相机
        this.camera = new THREE.PerspectiveCamera(45, this.container.clientWidth / this.container.clientHeight, 0.1, 1000);
        this.camera.position.set(15, 10, 15);
        this.camera.lookAt(0, 0, 0);

        // 渲染器
        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
        this.container.appendChild(this.renderer.domElement);

        // 轨道控制
        this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
        this.controls.enableDamping = true;

        // 灯光
        this.addLights();

        // 辅助元素
        this.addHelpers();

        // 绘制数据点
        this.drawPoints();

        // 动画循环
        this.animate();
    }

    addLights() {
        const ambientLight = new THREE.AmbientLight(0x404060);
        this.scene.add(ambientLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 1);
        dirLight.position.set(5, 10, 7);
        this.scene.add(dirLight);

        const backLight = new THREE.PointLight(0x4466cc, 0.5);
        backLight.position.set(-3, 2, -5);
        this.scene.add(backLight);
    }

    addHelpers() {
        // 网格辅助
        const gridHelper = new THREE.GridHelper(20, 20, 0x88aaff, 0x335588);
        gridHelper.position.y = -2;
        this.scene.add(gridHelper);

        // 简易坐标轴
        const axesHelper = new THREE.AxesHelper(8);
        this.scene.add(axesHelper);
    }

    drawPoints() {
        // 根据数据值映射颜色和大小
        const colorScale = d3.scaleSequential()
            .domain([0, 100])
            .interpolator(d3.interpolateViridis);

        this.data.forEach(point => {
            // 确定颜色
            const color = new THREE.Color(colorScale(point.value));

            // 确定大小(范围0.2-0.8)
            const size = 0.2 + (point.value / 100) * 0.6;

            // 创建球体
            const geometry = new THREE.SphereGeometry(size, 32, 32);
            const material = new THREE.MeshStandardMaterial({
                color: color,
                roughness: 0.3,
                metalness: 0.2,
                emissive: color,
                emissiveIntensity: 0.1
            });
            const sphere = new THREE.Mesh(geometry, material);
            sphere.position.set(point.x, point.y, point.z);
            sphere.userData = point;

            this.scene.add(sphere);
            this.points.push(sphere);
        });
    }

    animate() {
        requestAnimationFrame(() => this.animate());
        this.controls.update();
        this.renderer.render(this.scene, this.camera);
    }
}

第十部分:数据故事讲述

10.1 动态叙事可视化

// 动态叙事可视化 - 引导用户逐步理解数据
class NarrativeVisualization {
    constructor(containerId, data, storySteps) {
        this.container = document.getElementById(containerId);
        this.data = data;
        this.storySteps = storySteps;
        this.currentStep = 0;
        this.chart = null;
        this.annotations = [];

        this.init();
        this.setupNavigation();
    }

    init() {
        // 初始化基础图表
        this.chart = echarts.init(this.container);

        // 添加控制面板
        this.addControlPanel();

        // 加载第一个故事步骤
        this.loadStep(0);
    }

    addControlPanel() {
        const panel = document.createElement('div');
        panel.className = 'narrative-controls';
        panel.innerHTML = `
            <button id="prev-step" disabled>← 上一步</button>
            <span id="step-indicator">第 1 / ${this.storySteps.length} 步</span>
            <button id="next-step">下一步 →</button>
            <div class="step-progress">
                <div class="progress-bar" style="width: 0%"></div>
            </div>
        `;
        this.container.parentElement.insertBefore(panel, this.container);

        // 绑定事件
        document.getElementById('prev-step').onclick = () => this.prevStep();
        document.getElementById('next-step').onclick = () => this.nextStep();
    }

    loadStep(stepIndex) {
        const step = this.storySteps[stepIndex];
        if (!step) return;

        // 更新图表配置
        const option = this.buildChartOption(step);
        this.chart.setOption(option, true);

        // 添加标注
        this.addAnnotations(step.annotations);

        // 更新控制面板状态
        this.updateControls(stepIndex);

        // 更新旁白文本
        this.updateNarrative(step.narrative);

        // 高亮相关数据点
        if (step.highlight) {
            this.highlightDataPoints(step.highlight);
        }

        // 播放音效(可选)
        if (step.sound) {
            this.playSound(step.sound);
        }
    }

    buildChartOption(step) {
        // 基础配置
        const baseOption = {
            title: {
                text: step.title,
                left: 'center',
                textStyle: { fontSize: 18, fontWeight: 'bold' }
            },
            tooltip: { trigger: 'axis' },
            xAxis: { type: 'category', data: this.data.categories },
            yAxis: { type: 'value', name: step.yAxisLabel || '数值' },
            series: [{
                name: step.seriesName,
                type: step.chartType || 'bar',
                data: this.data.values,
                itemStyle: {
                    borderRadius: [4, 4, 0, 0],
                    color: step.colorScheme || '#5470c6'
                }
            }]
        };

        // 根据步骤添加特殊效果
        if (step.effect === 'explode') {
            // 爆炸效果:突出某个柱子
            const explodeIndex = step.explodeIndex;
            if (explodeIndex !== undefined) {
                baseOption.series[0].itemStyle = {
                    ...baseOption.series[0].itemStyle,
                    color: (params) => {
                        return params.dataIndex === explodeIndex ? '#ff6600' : '#5470c6';
                    }
                };
            }
        }

        if (step.effect === 'trend') {
            // 趋势线
            baseOption.series.push({
                name: '趋势线',
                type: 'line',
                data: step.trendData || this.data.values,
                smooth: true,
                lineStyle: { width: 3, color: '#ff6600', type: 'dashed' },
                symbol: 'none'
            });
        }

        return baseOption;
    }

    addAnnotations(annotations) {
        // 清除旧标注
        this.annotations.forEach(ann => ann.dispose());
        this.annotations = [];

        annotations.forEach(ann => {
            const graphic = {
                type: 'text',
                left: ann.left,
                top: ann.top,
                style: {
                    text: ann.text,
                    fill: ann.color || '#333',
                    fontSize: ann.fontSize || 14,
                    fontWeight: 'bold',
                    backgroundColor: 'rgba(255,255,255,0.8)',
                    padding: [5, 10, 5, 10],
                    borderRadius: 4
                },
                z: 100
            };
            this.chart.setOption({ graphic: graphic });
            this.annotations.push({ dispose: () => this.chart.setOption({ graphic: [] }) });
        });
    }

    highlightDataPoints(indices) {
        // 高亮指定数据点
        this.chart.dispatchAction({
            type: 'highlight',
            seriesIndex: 0,
            dataIndex: indices
        });

        // 3秒后取消高亮
        setTimeout(() => {
            this.chart.dispatchAction({
                type: 'downplay',
                seriesIndex: 0
            });
        }, 3000);
    }

    updateNarrative(text) {
        let narrativeDiv = document.getElementById('narrative-text');
        if (!narrativeDiv) {
            narrativeDiv = document.createElement('div');
            narrativeDiv.id = 'narrative-text';
            narrativeDiv.className = 'narrative-text';
            this.container.parentElement.appendChild(narrativeDiv);
        }

        // 打字机效果
        this.typeWriter(narrativeDiv, text);
    }

    typeWriter(element, text, speed = 30) {
        element.innerHTML = '';
        let i = 0;
        const timer = setInterval(() => {
            if (i < text.length) {
                element.innerHTML += text.charAt(i);
                i++;
            } else {
                clearInterval(timer);
            }
        }, speed);
    }

    updateControls(stepIndex) {
        const prevBtn = document.getElementById('prev-step');
        const nextBtn = document.getElementById('next-step');
        const indicator = document.getElementById('step-indicator');
        const progressBar = document.querySelector('.progress-bar');

        prevBtn.disabled = stepIndex === 0;
        nextBtn.disabled = stepIndex === this.storySteps.length - 1;
        indicator.textContent = `第 ${stepIndex + 1} / ${this.storySteps.length} 步`;
        progressBar.style.width = `${((stepIndex + 1) / this.storySteps.length) * 100}%`;
    }

    nextStep() {
        if (this.currentStep < this.storySteps.length - 1) {
            this.currentStep++;
            this.loadStep(this.currentStep);
        }
    }

    prevStep() {
        if (this.currentStep > 0) {
            this.currentStep--;
            this.loadStep(this.currentStep);
        }
    }
}

第十一部分:异常检测与数据质量可视化

11.1 异常检测可视化

// 异常检测可视化组件
class AnomalyDetectionChart {
    constructor(containerId, data) {
        this.container = document.getElementById(containerId);
        this.data = data;
        this.anomalies = [];
        this.chart = null;

        this.detectAnomalies();
        this.initChart();
    }

    // 基于3σ原则检测异常值
    detectAnomalies() {
        const values = this.data.map(d => d.value);
        const mean = values.reduce((a, b) => a + b, 0) / values.length;
        const stdDev = Math.sqrt(values.map(v => Math.pow(v - mean, 2)).reduce((a, b) => a + b, 0) / values.length);
        const threshold = 3 * stdDev;

        this.anomalies = this.data.filter(d => Math.abs(d.value - mean) > threshold);

        console.log(`检测到 ${this.anomalies.length} 个异常点`);
    }

    initChart() {
        this.chart = echarts.init(this.container);

        const option = {
            title: { text: '异常检测可视化', left: 'center' },
            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
            xAxis: { type: 'category', data: this.data.map(d => d.name) },
            yAxis: { type: 'value', name: '数值' },
            series: [
                {
                    name: '正常数据',
                    type: 'bar',
                    data: this.data.map(d => ({
                        value: d.value,
                        itemStyle: {
                            color: this.anomalies.includes(d) ? '#ff4444' : '#5470c6'
                        }
                    })),
                    itemStyle: {
                        borderRadius: [4, 4, 0, 0]
                    }
                },
                {
                    name: '均值线',
                    type: 'line',
                    data: Array(this.data.length).fill(this.getMean()),
                    lineStyle: { color: '#ffaa00', width: 2, type: 'dashed' },
                    symbol: 'none',
                    tooltip: { valueFormatter: (value) => `均值: ${value.toFixed(2)}` }
                },
                {
                    name: '上下限',
                    type: 'line',
                    data: Array(this.data.length).fill(this.getUpperLimit()),
                    lineStyle: { color: '#ff6666', width: 1, type: 'dotted' },
                    symbol: 'none'
                }
            ],
            visualMap: {
                show: false,
                pieces: [
                    { min: this.getUpperLimit(), color: '#ff4444' },
                    { max: this.getUpperLimit(), color: '#5470c6' }
                ]
            }
        };

        this.chart.setOption(option);
    }

    getMean() {
        const sum = this.data.reduce((a, b) => a + b.value, 0);
        return sum / this.data.length;
    }

    getStdDev() {
        const mean = this.getMean();
        const squaredDiffs = this.data.map(d => Math.pow(d.value - mean, 2));
        return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / this.data.length);
    }

    getUpperLimit() {
        return this.getMean() + 3 * this.getStdDev();
    }

    getLowerLimit() {
        return this.getMean() - 3 * this.getStdDev();
    }
}

第十二部分:地理信息可视化

12.1 地图散点图

// 地图散点图组件
class MapScatterPlot {
    constructor(containerId, data) {
        this.container = document.getElementById(containerId);
        this.data = data;
        this.map = null;

        this.initMap();
    }

    initMap() {
        // 使用Leaflet.js创建地图
        this.map = L.map(this.container).setView([39.9042, 116.4074], 5);

        // 添加底图图层
        L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors'
        }).addTo(this.map);

        // 添加数据点
        this.addMarkers();

        // 添加热力图
        if (this.data.length > 100) {
            this.addHeatmap();
        }
    }

    addMarkers() {
        this.data.forEach(point => {
            // 根据数值大小决定图标颜色和大小
            const size = 8 + (point.value / 100) * 20;
            const color = this.getValueColor(point.value);

            const circleMarker = L.circleMarker([point.lat, point.lng], {
                radius: size,
                fillColor: color,
                color: '#fff',
                weight: 1,
                opacity: 1,
                fillOpacity: 0.8
            }).addTo(this.map);

            // 添加弹窗
            circleMarker.bindPopup(`
                <strong>${point.name}</strong><br>
                数值: ${point.value}<br>
                分类: ${point.category}
            `);

            // 聚类效果(当缩放级别较小时)
            if (this.map.getZoom() < 8) {
                // 实现聚类逻辑
            }
        });
    }

    addHeatmap() {
        // 添加热力图图层
        const heatData = this.data.map(point => [point.lat, point.lng, point.value]);

        L.heatLayer(heatData, {
            radius: 25,
            blur: 15,
            maxZoom: 10,
            minOpacity: 0.3,
            gradient: {
                0.2: 'blue',
                0.4: 'cyan',
                0.6: 'lime',
                0.8: 'yellow',
                1.0: 'red'
            }
        }).addTo(this.map);
    }

    getValueColor(value) {
        // 根据数值范围返回颜色
        if (value < 20) return '#00ff00';
        if (value < 50) return '#ffff00';
        if (value < 80) return '#ff8800';
        return '#ff0000';
    }
}

第十三部分:可视化组件库封装

13.1 通用图表组件

// 通用图表组件类
class ChartComponent {
    constructor(containerId, config) {
        this.container = document.getElementById(containerId);
        this.config = config;
        this.chart = null;
        this.data = [];
        this.eventHandlers = new Map();

        this.init();
    }

    init() {
        // 根据配置创建对应类型的图表
        const chartTypes = {
            'line': echarts,
            'bar': echarts,
            'pie': echarts,
            'scatter': echarts,
            'heatmap': echarts
        };

        const ChartLib = chartTypes[this.config.type] || echarts;
        this.chart = ChartLib.init(this.container);

        // 设置基础配置
        this.setBaseOption();

        // 绑定窗口大小变化事件
        window.addEventListener('resize', () => this.resize());
    }

    setBaseOption() {
        const baseOption = {
            title: {
                text: this.config.title,
                left: 'center',
                textStyle: { fontSize: 16, fontWeight: 'normal' }
            },
            tooltip: { trigger: 'axis' },
            legend: { data: this.config.legend, bottom: 0 },
            grid: {
                left: '10%',
                right: '10%',
                containLabel: true
            },
            toolbox: {
                feature: {
                    saveAsImage: { title: '保存为图片' },
                    restore: { title: '重置' },
                    dataZoom: { title: '区域缩放' }
                }
            }
        };

        this.chart.setOption(baseOption);
    }

    setData(data) {
        this.data = data;
        this.update();
    }

    update() {
        const option = this.buildOption();
        this.chart.setOption(option);
    }

    buildOption() {
        // 子类重写
        return {};
    }

    on(event, handler) {
        this.chart.on(event, handler);
        this.eventHandlers.set(event, handler);
    }

    off(event) {
        if (this.eventHandlers.has(event)) {
            this.chart.off(event, this.eventHandlers.get(event));
            this.eventHandlers.delete(event);
        }
    }

    resize() {
        this.chart.resize();
    }

    dispose() {
        this.chart.dispose();
        this.container.innerHTML = '';
    }
}

// 柱状图组件
class BarChart extends ChartComponent {
    buildOption() {
        return {
            ...super.buildOption(),
            xAxis: {
                type: 'category',
                data: this.data.categories,
                axisLabel: { rotate: 45, interval: 0 }
            },
            yAxis: { type: 'value', name: this.config.yAxisName },
            series: [{
                name: this.config.seriesName,
                type: 'bar',
                data: this.data.values,
                itemStyle: {
                    borderRadius: [4, 4, 0, 0],
                    color: this.config.color || '#5470c6'
                },
                label: {
                    show: this.config.showLabel || false,
                    position: 'top'
                }
            }]
        };
    }
}

// 折线图组件
class LineChart extends ChartComponent {
    buildOption() {
        return {
            ...super.buildOption(),
            xAxis: {
                type: 'category',
                data: this.data.categories,
                boundaryGap: false
            },
            yAxis: { type: 'value', name: this.config.yAxisName },
            series: [{
                name: this.config.seriesName,
                type: 'line',
                data: this.data.values,
                smooth: this.config.smooth || true,
                lineStyle: { width: 3 },
                areaStyle: this.config.area ? { opacity: 0.3 } : undefined,
                symbol: 'circle',
                symbolSize: 8,
                itemStyle: { color: this.config.color || '#5470c6' }
            }]
        };
    }
}

// 饼图组件
class PieChart extends ChartComponent {
    buildOption() {
        return {
            ...super.buildOption(),
            tooltip: { trigger: 'item' },
            series: [{
                name: this.config.seriesName,
                type: 'pie',
                radius: ['40%', '70%'],
                avoidLabelOverlap: false,
                itemStyle: {
                    borderRadius: 10,
                    borderColor: '#fff',
                    borderWidth: 2
                },
                label: {
                    show: true,
                    formatter: '{b}: {d}%'
                },
                emphasis: {
                    scale: true,
                    label: { show: true, fontWeight: 'bold' }
                },
                data: this.data.map(item => ({
                    name: item.name,
                    value: item.value,
                    itemStyle: { color: item.color }
                }))
            }]
        };
    }
}

数据可视化是一门融合了设计美学、数据科学和编程技术的综合性学科。本文从多个维度系统性地讲解了可视化技术的实现方法:
3D可视化:使用Three.js创建沉浸式的三维数据展示
数据故事讲述:通过动态叙事引导用户理解数据
异常检测可视化:帮助快速识别数据质量问题
地理信息可视化:在地图上展示数据的空间分布
组件化封装:构建可复用的可视化组件库
数据可视化的核心始终是让数据说话,技术只是工具,洞察才是目的。
来源:https://bncne.cn/

相关文章
|
8天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
34493 21
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
19天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
45349 142
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
2天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
2773 8
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
9天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4978 21
|
1天前
|
人工智能 监控 安全
阿里云SASE 2.0升级,全方位监控Agent办公安全
AI Agent办公场景的“安全底座”
1132 1
|
7天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1937 6
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案