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

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战篇38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?相关的知识,希望对你有一定的参考价值。

说明

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

图表库 vs 数据驱动框架

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

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

文档

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>

实现效果如下:

使用 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>

以上是关于实战篇38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?的主要内容,如果未能解决你的问题,请参考以下文章

D3.js

数据可视化图表框架归纳

怎么学习d3.js

D3.js使用简书

potree的第三方库

D3.js的V5版本-Vue框架中使用-树状图