【实战篇】38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?

简介: 【实战篇】38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?

说明

【跟月影学可视化】学习笔记。



图表库 vs 数据驱动框架


  • 图表库只要调用 API 就能展现内容,灵活性不高,对数据格式要求也很严格,但方便
  • 数据驱动框架需要手动去完成内容的呈现,灵活,不受图表类型对应 API 的制约,但不方便


b19e139ef1974de89bd8c900e80d448d.png


数据驱动框架不要求固定格式的数据格式,而是通过对原始数据的处理和对容器迭代、创建新的子元素,并且根据数据设置属性,来完成从数据到元素结构和属性的映射,然后再用渲染引擎将它最终渲染出来。当需求比较复杂,或者样式要求灵活多变的时候,可以考虑使用数据驱动框架。




文档


d3js 文档以及 spritejs 文档



d3-selection 依赖于 DOM 操作,所以 SVG 和 SpriteJS 这种与 DOM API 保持一致的图形系统,使用起来会更加方便一些。下面将使用这个两个库进行demo的演示



使用 D3.js 绘制条形图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 D3.js 绘制条形图</title>
    <style>
        html, body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            padding: 40px;
            margin: 0;
        }
        #stage {
            display: inline-block;
            width: 1200px;
            height: 600px;
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <div id="stage"></div>
    <script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script>
    <script src="https://d3js.org/d3.v6.js"></script>
    <script>
        const { Scene, SpriteSvg } = spritejs;
        const container = document.getElementById('stage');
        // 先创建一个 Scene 对象
        const scene = new Scene({
            container,
            width: 600,
            height: 600,
        });
        // 数组数据
        const dataset = [125, 121, 127, 193, 309];
        // 使用 D3.js 的方法对数据进行映射
        // scale 函数把一组数值线性映射到某个范围,下面就是将数值映射到 500 像素区间,数值是从 100 到 309。
        const scale = d3.scaleLinear()
            .domain([100, d3.max(dataset)])
            .range([0, 500]);
        // 创建了一个 fglayer,它对应一个 Canvas 画布
        const fglayer = scene.layer('fglayer');
        // 将对应的 fglayer 元素经过 d3 包装后返回
        const s = d3.select(fglayer);
        const colors = ['#fe645b', '#feb050', '#c2af87', '#81b848', '#55abf8'];
        // 在 fglayer 元素上进行迭代操作,selectAll 用来返回 fglayer 下的 sprite 子元素,表示一个图形
        // 通过执行 enter() 和 append(‘sprite’),在 fglayer 下添加了 5 个 sprite 子元素。
        // 再给每个 sprite 元素迭代设置属性,不同的值,就通过迭代算子来设置。
        const chart = s.selectAll('sprite')
            .data(dataset)
            .enter()
            .append('sprite')
            .attr('x', 20)
            .attr('y', (d, i) => {
                return 40 + i * 95;
            })
            .attr('width', scale)
            .attr('height', 80)
            .attr('bgcolor', (d, i) => {
                return colors[i];
            });
        // 添加坐标轴
        // 通过 d3.axisBottom 创建一个底部的坐标,通过 tickValues 给坐标轴传要显示的刻度值 100, 200, 300
        // 返回的 axis 函数用来绘制坐标轴,它是使用 svg 来绘制坐标轴的
        const axis = d3.axisBottom(scale).tickValues([100, 200, 300]);
        // SpriteSvg 可以绘制一个 SVG 图形,然后将这个图形以 WebGL 或者 Canvas2D 的方式绘制到画布上。
        const axisNode = new SpriteSvg({
            x: 0,
            y: 520,
        });
        // 通过 d3.select 选中 axisNode 对象的 svg 属性进行 svg 属性设置和创建 svg 元素操作
        d3.select(axisNode.svg)
            .attr('width', 600)
            .attr('height', 520)
            .append('g')
            .attr('transform', 'translate(20, 0)')
            .call(axis);
        axisNode.svg.children[0].setAttribute('font-size', 20);
        // 将 axisNode 添加到 fglayer 上
        fglayer.append(axisNode);
    </script>
</body>
</html>

实现效果如下:

fc6d62a1b3154bb183131ea29e040344.png


使用 D3.js 绘制力导向图


力导向图通过模拟节点之间的斥力,来保证节点不会相互重叠。不仅能够描绘节点和关系链,而且在移动一个节点的时候,图表各个节点的位置会跟随移动,避免节点相互重叠。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 D3.js 绘制力导向图</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            padding: 0;
            margin: 0;
        }
        #stage {
            display: inline-block;
            width: 100%;
            height: 0;
            padding-bottom: 75%;
        }
        #stage canvas {
            background-color: seashell;
        }
    </style>
</head>
<body>
    <div id="stage"></div>
    <script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script>
    <script src="https://d3js.org/d3.v6.js"></script>
    <script>
        const { Scene } = spritejs;
        console.log(Scene);
        const container = document.getElementById('stage');
        // 先创建一个 Scene 对象
        const scene = new Scene({
            container,
            width: 1200,
            height: 900,
            mode: 'stickyWidth'
        });
        // 创建了一个 fglayer,它对应一个 Canvas 画布
        const layer = scene.layer('fglayer', {
            handleEvent: false,
            autoRender: false,
        });
        // 创建一个 d3 的力模型对象 simulation
        const simulation = d3.forceSimulation()
            .force('link', d3.forceLink().id(d => d.id)) //节点连线 
            .force('charge', d3.forceManyBody()) // 多实体作用
            .force('center', d3.forceCenter(400, 300)); // 力中心
        // 用 d3.json 来读取数据,它返回一个 Promise 对象
        d3.json('./data/FeHelper-20230106175037.json').then(graph => {
            console.log(graph);
            function ticked() {
                d3.select(layer).selectAll('path')
                    .attr('d', (d) => {
                        const [sx, sy] = [d.source.x, d.source.y];
                        const [tx, ty] = [d.target.x, d.target.y];
                        return `M${sx} ${sy} L ${tx} ${ty}`;
                    })
                    .attr('strokeColor', 'salmon')
                    .attr('lineWidth', 1);
                d3.select(layer).selectAll('sprite')
                    .attr('pos', (d) => {
                        return [d.x, d.y];
                    });
                layer.render();
            }
            // 先用力模型来处理数据
            simulation.nodes(graph.nodes).on('tick', ticked);
            simulation.force('link').links(graph.links);
            // 再绘制节点
            d3.select(layer).selectAll('sprite')
                .data(graph.nodes)
                .enter()
                .append('sprite')
                .attr('pos', (d) => {
                    return [d.x, d.y];
                })
                .attr('size', [10, 10])
                .attr('border', [1, 'salmon'])
                .attr('borderRadius', 5)
                .attr('anchor', 0.5);
            // 再绘制连线
            d3.select(layer).selectAll('path')
                .data(graph.links)
                .enter()
                .append('path')
                .attr('d', (d) => {
                    const [sx, sy] = [d.source.x, d.source.y];
                    const [tx, ty] = [d.target.x, d.target.y];
                    return `M${sx} ${sy} L ${tx} ${ty}`;
                })
                .attr('name', (d, index) => {
                    return `path${index}`;
                })
                .attr('strokeColor', 'salmon');
            function dragsubject() {
                const [x, y] = layer.toLocalPos(event.x, event.y);
                return simulation.find(x, y);
            }
            // 将三个事件处理函数注册到 layer 的 canvas 上
            d3.select(layer.canvas)
                .call(d3.drag()
                    .container(layer.canvas)
                    .subject(dragsubject)
                    .on('start', dragstarted)
                    .on('drag', dragged)
                    .on('end', dragended)
                );
        });
        // dragstarted 处理开始拖拽的事件
        function dragstarted(event) {
            // 通过前面创建的 simulation 对象启动力模拟,记录一下当前各个节点的 x、y 坐标
            if (!event.active) simulation.alphaTarget(0.3).restart();
            const [x, y] = [event.subject.x, event.subject.y];
            event.subject.fx0 = x;
            event.subject.fy0 = y;
            event.subject.fx = x;
            event.subject.fy = y;
            // 通过 layer.toLocalPos 方法将它转换成相对于 layer 的坐标
            const [x0, y0] = layer.toLocalPos(event.x, event.y);
            event.subject.x0 = x0;
            event.subject.y0 = y0;
        }
        // dragged 处理拖拽中的事件
        function dragged(event) {
            // 转换 x、y 坐标,计算出坐标的差值,然后更新 fx、fy
            const [x, y] = layer.toLocalPos(event.x, event.y),
                { x0, y0, fx0, fy0 } = event.subject;
            const [dx, dy] = [x - x0, y - y0];
            event.subject.fx = fx0 + dx;
            event.subject.fy = fy0 + dy;
        }
        // dragended 处理拖住结束事件,清空 fx 和 fy
        function dragended(event) {
            if (!event.active) simulation.alphaTarget(0);
            event.subject.fx = null;
            event.subject.fy = null;
        }
    </script>
</body>
</html>

636b73e6bbc14a6bbbc169583bdeda14.gif

目录
相关文章
|
8天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端框架
【10月更文挑战第34天】在数字化时代,后端开发如同一座桥梁,连接着用户界面与数据处理的两端。本文将通过Node.js这一轻量级、高效的平台,带领读者领略后端框架的魅力。我们将从基础概念出发,逐步深入到实战应用,最后探讨如何通过代码示例来巩固学习成果,使读者能够在理论与实践之间架起自己的桥梁。
|
30天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
1月前
|
JavaScript 前端开发 API
Vue.js:现代前端开发的强大框架
【10月更文挑战第11天】Vue.js:现代前端开发的强大框架
65 41
|
14天前
|
JavaScript 中间件 API
Node.js进阶:Koa框架下的RESTful API设计与实现
【10月更文挑战第28天】本文介绍了如何在Koa框架下设计与实现RESTful API。首先概述了Koa框架的特点,接着讲解了RESTful API的设计原则,包括无状态和统一接口。最后,通过一个简单的博客系统示例,详细展示了如何使用Koa和koa-router实现常见的CRUD操作,包括获取、创建、更新和删除文章。
35 4
|
16天前
|
数据采集 存储 JavaScript
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
本文介绍了如何使用Puppeteer和Node.js爬取大学招生数据,并通过代理IP提升爬取的稳定性和效率。Puppeteer作为一个强大的Node.js库,能够模拟真实浏览器访问,支持JavaScript渲染,适合复杂的爬取任务。文章详细讲解了安装Puppeteer、配置代理IP、实现爬虫代码的步骤,并提供了代码示例。此外,还给出了注意事项和优化建议,帮助读者高效地抓取和分析招生数据。
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
|
21天前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
26 1
|
21天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的完美结合
【10月更文挑战第21天】本文将引导你走进Node.js和Express框架的世界,探索它们如何共同打造一个高效、可扩展的后端服务。通过深入浅出的解释和实际代码示例,我们将一起理解这一组合的魅力所在,并学习如何利用它们来构建现代Web应用。
40 1
|
1月前
|
SQL 前端开发 JavaScript
Nest.js 实战 (十五):前后端分离项目部署的最佳实践
这篇文章介绍了如何使用现代前端框架Vue3和后端Node.js框架Nest.js实现的前后端分离架构的应用,并将其部署到生产环境。文章涵盖了准备阶段,包括云服务器的设置、1Panel面板的安装、数据库的安装、域名的实名认证和备案、SSL证书的申请。在部署Node服务环节,包括了Node.js环境的创建、数据库的配置、用户名和密码的设置、网站信息的填写、静态网站的部署、反向代理的配置以及可能遇到的常见问题。最后,作者总结了部署经验,并希望对读者有所帮助。
127 11
|
1月前
|
前端开发 JavaScript
JS-数据筛选
JS-数据筛选
33 7
|
9天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。