【实战篇】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

目录
相关文章
|
5月前
|
人工智能 自然语言处理 JavaScript
通义灵码2.5实战评测:Vue.js贪吃蛇游戏一键生成
通义灵码基于自然语言需求,快速生成完整Vue组件。例如,用Vue 2和JavaScript实现贪吃蛇游戏:包含键盘控制、得分系统、游戏结束判定与Canvas动态渲染。AI生成的代码符合规范,支持响应式数据与事件监听,还能进阶优化(如增加启停按钮、速度随分数提升)。传统需1小时的工作量,使用通义灵码仅10分钟完成,大幅提升开发效率。操作简单:安装插件、输入需求、运行项目即可实现功能。
253 4
 通义灵码2.5实战评测:Vue.js贪吃蛇游戏一键生成
|
14天前
|
JavaScript 前端开发 安全
【逆向】Python 调用 JS 代码实战:使用 pyexecjs 与 Node.js 无缝衔接
本文介绍了如何使用 Python 的轻量级库 `pyexecjs` 调用 JavaScript 代码,并结合 Node.js 实现完整的执行流程。内容涵盖环境搭建、基本使用、常见问题解决方案及爬虫逆向分析中的实战技巧,帮助开发者在 Python 中高效处理 JS 逻辑。
|
5月前
|
Web App开发 数据采集 JavaScript
动态网页爬取:Python如何获取JS加载的数据?
动态网页爬取:Python如何获取JS加载的数据?
892 58
|
3月前
|
JavaScript 前端开发 算法
流量分发代码实战|学会用JS控制用户访问路径
流量分发工具(Traffic Distributor),又称跳转器或负载均衡器,可通过JavaScript按预设规则将用户随机引导至不同网站,适用于SEO优化、广告投放、A/B测试等场景。本文分享一段不到百行的JS代码,实现智能、隐蔽的流量控制,并附完整示例与算法解析。
91 1
|
6月前
|
JavaScript 前端开发 API
|
7月前
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
|
7月前
|
数据采集 JavaScript 前端开发
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
用array.filter()来实现数据筛选、数据清洗和链式调用,相对于for循环更加清晰,语义化强,能显著提升代码的可读性和可维护性。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
监控 安全 中间件
Next.js 实战 (十):中间件的魅力,打造更快更安全的应用
这篇文章介绍了什么是Next.js中的中间件以及其应用场景。中间件可以用于处理每个传入请求,比如实现日志记录、身份验证、重定向、CORS配置等功能。文章还提供了一个身份验证中间件的示例代码,以及如何使用限流中间件来限制同一IP地址的请求次数。中间件相当于一个构建模块,能够简化HTTP请求的预处理和后处理,提高代码的可维护性,有助于创建快速、安全和用户友好的Web体验。
202 0
Next.js 实战 (十):中间件的魅力,打造更快更安全的应用
|
中间件 API
Next.js 实战 (八):使用 Lodash 打包构建产生的“坑”?
这篇文章介绍了作者在使用Nextjs15进行项目开发时遇到的部署问题。在部署过程中,作者遇到了打包构建时的一系列报错,报错内容涉及动态代码评估在Edge运行时不被允许等问题。经过一天的尝试和调整,作者最终删除了lodash-es库,并将radash的部分源码复制到本地,解决了打包报错的问题。文章最后提供了项目的线上预览地址,并欢迎读者留言讨论更好的解决方案。
231 0
Next.js 实战 (八):使用 Lodash 打包构建产生的“坑”?
|
设计模式 数据安全/隐私保护
Next.js 实战 (七):浅谈 Layout 布局的嵌套设计模式
这篇文章介绍了在Next.js框架下,如何处理中后台管理系统中特殊页面(如登录页)不包裹根布局(RootLayout)的问题。作者指出Next.js的设计理念是通过布局的嵌套来创建复杂的页面结构,这虽然保持了代码的整洁和可维护性,但对于特殊页面来说,却造成了不必要的布局包裹。文章提出了一个解决方案,即通过判断页面的skipGlobalLayout属性来决定是否包含RootLayout,从而实现特殊页面不包裹根布局的目标。
296 0
Next.js 实战 (七):浅谈 Layout 布局的嵌套设计模式