20、数据可视化:Voronoi图与热力图的实现与应用

数据可视化:Voronoi图与热力图的实现与应用

1. Voronoi图的生成与嵌套

在数据可视化的领域中,Voronoi图是一种非常有趣的可视化形式。我们可以使用不同的投影方式,例如:

var projection = d3.geoPolyhedralWaterman()
var color = function() {return d3.interpolateBrBG(Math.random())};

1.1 嵌套Voronoi图的生成艺术

除了基于真实数据创建Voronoi图,我们还可以使用随机数据来生成。在这部分,我们将更进一步,在已有的Voronoi图的单元格内创建额外的Voronoi图,从而得到嵌套的Voronoi图。

1.2 设置Voronoi图

首先,我们需要设置渲染整个Voronoi图的区域,以下是标准的设置代码:

var pointSeed = 8;
// 通用设置
var margin = {top: 50, bottom: 20, right: 20, left: 20},
    width = 1200 - margin.left - margin.right,
    height = 600 - margin.top - margin.bottom;
// 创建标准图表
var svg = d3.select(".chart")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height+ margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left +
       "," + margin.top + ")");
var defs = d3.select(".chart").append("defs");
var color = function() {return d3.interpolateRainbow(Math.random())};

这里定义了 pointSeed ,它决定了我们用于生成Voronoi图的随机点的数量。同时,我们添加了 defs 部分用于添加裁剪路径,并定义了一个简单的随机颜色生成器。

1.3 渲染第一层Voronoi图

对于第一层,我们要为整个区域创建Voronoi图。我们使用 generateRandomPoints 函数生成随机点:

var voronoi = d3.voronoi().extent([[-1, -1], [width + 1, height + 1]]);
var points = generateRandomPoints(pointSeed, 0, width, 0, height);
var polygons = voronoi.polygons(points);
drawVoronoi(svg, polygons, undefined, 0);
function generateRandomPoints(nPoints, minX, maxX, minY, maxY) {
    return d3.range(0, nPoints).map( function(i) {
        return [Math.floor(Math.random() * (maxX-minX)) + minX,
Math.floor(Math.random() * (maxY-minY)) + minY]
    })
}

然后使用 drawVoronoi 函数绘制这些多边形:

function drawVoronoi(parent, polygons, clipArea, level) {
    parent.insert("g",":first-child")
        .attr("clip-path", function(d) { return clipArea ?
           "url(#" + clipArea+ ")" : ""})
        .attr("class", "polygons")
        .selectAll("path")
        .data(polygons)
        .enter().insert("path")
        .attr("data-level",level)
        .attr("stroke-width", function()
            {return 6 / ((level+1)*2) })
        .attr("stroke", function()
{d3.hsl(
           "#000").brighter(level)})
        .attr("fill", function()
{return level
           === 0 ? "" : color()})
        .attr("fill-opacity", "0.3")
        .attr("d", polyToPath)
}
function polyToPath(polygon) {
    return polygon ? "M" + polygon.join("L") + "Z" : null;
}

这个函数接受多个参数,用于绘制不同层级的Voronoi图。我们通过设置 stroke-width stroke 颜色,使得Voronoi图的轮廓随着层级加深而变细。

1.4 递归创建Voronoi图

为了绘制嵌套的多边形,我们使用以下代码:

// 绘制嵌套多边形
d3.range(1,4).forEach(drawSubPolygons);
function drawSubPolygons(level) {
    var parentLevel = level-1;
    // 处理每个父多边形
    var selection = d3.selectAll('path[data-level="'
       + parentLevel +'"]');
    var totalPolygons = [];
    selection.each(function(d, i) {
        var box = this.getBBox();
        // 生成随机点
var points20 = generateRandomPoints(pointSeed * level,
           box.x, box.x + box.width, box.y, box.y + box.height);
        // 定义新Voronoi图的范围
var voronoi2 =
d3.voronoi().extent([[box.x, box.y],
           [box.x + box.width, box.y+box.height]]);
        var polygons2 = voronoi2.polygons(points20)
        // 绘制新的Voronoi图
        if (polygons2.length > 0) {
            drawVoronoi(d3.select(this.parentNode), polygons2,
               "cp-" + parentLevel + "-" + i, level);
            addClipPath(d, "cp-" + parentLevel + "-" + i);
        }
    });
}

这里我们通过循环调用 drawSubPolygons 函数,绘制了层级1、2和3的Voronoi图。在每个父单元格内创建新的Voronoi图,并使用裁剪路径限制其显示区域。

以下是生成嵌套Voronoi图的流程图:

graph TD;
    A[设置Voronoi图区域] --> B[渲染第一层Voronoi图];
    B --> C[递归创建嵌套Voronoi图];

2. 电影脏话使用热力图的创建

接下来,我们将创建一个热力图,用于展示电影中脏话的使用频率。

2.1 数据准备

我们通过分析电影字幕来确定脏话的使用量。首先,我们需要下载电影的字幕文件(.srt格式)和一个脏话列表。以下是处理数据的脚本:

const d3 = require('d3');
const fs = require('fs');
const srt = require('srt');
const _ = require('lodash');
var timePeriod = 30;
d3.queue()
    .defer(d3.text, "file:dwList.txt")
    .defer(d3.text, "file:swearnet.srt")
    .await(function (error, dwList, subs) {
        var dirties = dwList.split("\n");
        var srts = srt.fromString(subs.replace(/\r\n/g, "\n"));
        var timedWords = [];
        var allKeys = Object.keys(srts);
        allKeys.forEach(function(key) {
            var startTime = srts[key].startTime;
            var endTime = srts[key].endTime;
            var totalTime = endTime - startTime;
            var words = srts[key].text.match(/[a-zA-Z]+/g);
            if (words) {
                var timePerCharacter = totalTime / words.length;
                words.forEach(function(word, i) {
                    timedWords.push({
                        word: word,
                        time: startTime + (timePerCharacter * i)
                    });
                });
            }
        });
        var lastElement = srts[allKeys.slice(-1)[0]];
        var endTime = lastElement.endTime;
        var onlySW = timedWords.filter(function(timed) {
            return _.includes(dirties, timed.word)
        })
        var grouped = _.groupBy(onlySW, function(timed) {
            return Math.floor((timed.time / (1000 * timePeriod)));
        })
        var groupedPerMinute = d3.range(0, Math.floor(endTime /
           (1000 * timePeriod)) + 1).map(function(i) {
            return grouped[i] ?
            { minute: i, count: grouped[i].length} :
            { minute: i, count: 0}
        });
        fs.writeFile('./sw-1.csv', d3.csvFormat(groupedPerMinute))
    });

从高层次来看,我们执行以下步骤:
1. 读取脏话列表和字幕文件。
2. 将字幕文件拆分为单独的单词,并确定每个单词的大致发言时间。
3. 通过与脏话列表对比,统计脏话数量。
4. 按时间间隔分组脏话,并将结果写入CSV文件。

2.2 渲染热力图

为了渲染热力图,我们需要执行以下步骤:
1. 为输入CSV中的每个元素绘制单独的矩形。
2. 在可视化的左侧添加额外信息,显示电影中的时间。
3. 在底部提供一个图例,说明颜色编码的含义。

以下是配置热力图外观的代码:

// 热力图配置
var elementsPerLine = 30,
    gridSize = Math.floor(width / elementsPerLine),
    legendElementWidth = gridSize * 2,
    buckets = 5,
    secondsPerElement = 30;

我们还创建了一个辅助函数来渲染CSV数据:

function show() {
    addDiagram("data/TheBigLebowski.csv", "The Big Lebowski");
}
var addDiagram = function (tsvFile, title) {
    var chartGroup = svg.append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top +
")");
    var textGroup = svg.append("g");
    d3.csv(tsvFile,
        function (d) {
            var minute = +d.minute;
            var column = minute % elementsPerLine;
            return {
                column: column,
                value: +d.count,
                row: Math.floor(minute / elementsPerLine)
            };
        },
        function (error, data) {
            var totalHeight = (Math.ceil(data.length / elementsPerLine)) *
(gridSize) + margin.top + margin.bottom;
            svg.attr('height', totalHeight);
            var maxValue = d3.max(data, function(d) {return d.value});
            var colorScale = d3.scaleQuantize()
                .range(d3.range(0, buckets ).map(function(i) {
                    return d3.interpolateReds((i+1)/buckets);
                }))
                .domain([0, maxValue]);
            chartGroup.selectAll("rect")
                .data(data)
                .enter().append("rect")
                .attr("x", function (d) { return (d.column) * gridSize; })
                .attr("y", function (d) { return (d.row - 1) * gridSize; })
                .attr("rx", 4)
                .attr("ry", 4)
                .attr("class", "column bordered")
                .attr("width", gridSize)
                .attr("height", gridSize)
                .style("fill", function(d) {return(colorScale(d.value))});
        }
    );
}

2.3 添加分钟信息和图例

在热力图的左侧添加分钟信息:

function (error, data) {
  chartGroup.selectAll("text")
    .data(d3.range(0, Math.ceil(data.length / elementsPerLine)))
    .enter().append("text")
    .text(function(d, i) {
      return Math.round(i * elementsPerLine * secondsPerElement / 60) +
"m."
      })
    .attr("class", "mono")
    .attr("text-anchor", "end")
    .attr("y", function(d, i) {return i * gridSize})
    .attr("dx", -5)
    .attr("dy", -5)
}

添加图例:

var legendPos = ((Math.floor(data.length / elementsPerLine) + 1) *
(gridSize));
var legend = chartGroup.selectAll(".legend")
    .data(d3.range(0, buckets))
var newLegends = legend.enter().append("g")
    .attr("class", "legend")
newLegends.append("rect")
    .attr("x", function (d, i) { return legendElementWidth * i; })
    .attr("y", legendPos)
    .attr("width", legendElementWidth)
    .attr("height", gridSize / 2)
    .style("fill", function (d, i) { return colorScale.range()[i]; })
newLegends.append("text")
    .attr("class", "mono")
    .text(function (d, i) { return "≥ " + Math.round(maxValue / (buckets) *
i); })
    .attr("x", function (d, i) { return legendElementWidth * i; })
    .attr("y", legendPos + gridSize);

以下是创建电影脏话使用热力图的流程图:

graph TD;
    A[准备数据] --> B[渲染热力图];
    B --> C[添加分钟信息];
    C --> D[添加图例];

通过以上步骤,我们成功地创建了嵌套Voronoi图和电影脏话使用热力图。通过调整参数和插值器,我们可以轻松创建不同的可视化效果。

3. D3中的自定义形状与路径及刷子选择

在数据可视化中,除了Voronoi图和热力图,D3还提供了许多其他有趣的功能,如自定义形状、路径操作、SVG的导入导出以及刷子选择等。

3.1 D3支持的符号

D3支持多种类型的符号,这些符号实际上是小型的SVG路径,例如十字、菱形等。我们可以使用符号来更好地注释散点图或折线图。以下是一个简单的使用符号的示例:

// 假设我们有一个SVG元素
var svg = d3.select("body").append("svg")
    .attr("width", 500)
    .attr("height", 500);

// 创建一个符号生成器
var symbol = d3.symbol()
    .type(d3.symbolCross) // 选择十字符号
    .size(100);

// 添加符号到SVG
svg.append("path")
    .attr("d", symbol())
    .attr("transform", "translate(250, 250)")
    .attr("fill", "blue");

在这个示例中,我们首先创建了一个SVG元素,然后使用 d3.symbol 生成器创建了一个十字符号,并将其添加到SVG中。

3.2 直接构建路径

我们已经看到不同的路径生成器使用路径,实际上我们也可以直接构建路径。下面是一个使用 d3.path 函数从 scratch 创建路径的示例:

// 创建一个路径
var path = d3.path();

// 移动到起始点
path.moveTo(100, 100);

// 绘制一条线到另一个点
path.lineTo(200, 200);

// 关闭路径
path.closePath();

// 创建一个SVG元素
var svg = d3.select("body").append("svg")
    .attr("width", 500)
    .attr("height", 500);

// 将路径添加到SVG
svg.append("path")
    .attr("d", path.toString())
    .attr("stroke", "red")
    .attr("fill", "none");

在这个示例中,我们使用 d3.path 函数创建了一个路径,并通过 moveTo lineTo closePath 方法定义了路径的形状,最后将路径添加到SVG中。

3.3 SVG的导出

使用D3可以创建漂亮的可视化效果,但这些效果通常只能在支持JavaScript的浏览器中访问。我们可以将D3可视化导出为PNG和SVG文件。以下是一个简单的导出SVG的示例:

// 假设我们有一个SVG元素
var svg = d3.select("svg");

// 获取SVG的内容
var svgData = new XMLSerializer().serializeToString(svg.node());

// 创建一个Blob对象
var svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });

// 创建一个URL
var svgUrl = URL.createObjectURL(svgBlob);

// 创建一个链接元素
var downloadLink = document.createElement("a");
downloadLink.href = svgUrl;
downloadLink.download = "visualization.svg";

// 模拟点击链接进行下载
downloadLink.click();

在这个示例中,我们首先获取SVG的内容,然后将其转换为Blob对象,创建一个URL,最后通过创建一个链接元素并模拟点击来实现下载。

3.4 SVG的导入

在之前的章节中,我们已经多次展示了如何从外部文件导入SVG元素。这里我们将更深入地了解如何使用Inkscape(一个开源的SVG编辑器)创建或修改SVG图像,并将结果文件导入到D3中。以下是一个简单的导入SVG的示例:

// 加载SVG文件
d3.xml("path/to/your/svg/file.svg")
  .then(function(data) {
    // 将SVG内容添加到页面
    d3.select("body").node().appendChild(data.documentElement);
  })
  .catch(function(error) {
    console.log("Error loading SVG: ", error);
  });

在这个示例中,我们使用 d3.xml 函数加载SVG文件,并将其内容添加到页面中。

3.5 刷子选择

最后,我们将介绍如何使用刷子选择多个D3元素。以下是一个随机显示一些符号并使用刷子进行选择的示例:

// 创建一个SVG元素
var svg = d3.select("body").append("svg")
    .attr("width", 500)
    .attr("height", 500);

// 生成一些随机点
var points = d3.range(20).map(function() {
    return [Math.random() * 500, Math.random() * 500];
});

// 创建一个符号生成器
var symbol = d3.symbol()
    .type(d3.symbolCircle)
    .size(50);

// 添加符号到SVG
svg.selectAll("path")
    .data(points)
    .enter().append("path")
    .attr("d", symbol)
    .attr("transform", function(d) { return "translate(" + d + ")"; })
    .attr("fill", "green");

// 创建一个刷子
var brush = d3.brush()
    .extent([[0, 0], [500, 500]])
    .on("end", brushed);

// 添加刷子到SVG
svg.append("g")
    .attr("class", "brush")
    .call(brush);

// 刷子事件处理函数
function brushed() {
    var selection = d3.event.selection;
    if (selection) {
        svg.selectAll("path")
            .attr("fill", function(d) {
                return isPointInSelection(d, selection) ? "red" : "green";
            });
    }
}

// 判断点是否在选择区域内
function isPointInSelection(point, selection) {
    var x = point[0], y = point[1];
    var [[x0, y0], [x1, y1]] = selection;
    return x >= x0 && x <= x1 && y >= y0 && y <= y1;
}

在这个示例中,我们首先随机生成了一些点,并在SVG中添加了圆形符号。然后创建了一个刷子,并将其添加到SVG中。当刷子选择发生变化时,我们会根据选择区域更新符号的颜色。

以下是使用D3进行自定义形状、路径操作、SVG导入导出和刷子选择的流程图:

graph TD;
    A[使用D3符号] --> B[直接构建路径];
    B --> C[导出SVG];
    C --> D[导入SVG];
    D --> E[刷子选择];

总结

通过本文,我们学习了如何创建嵌套Voronoi图和电影脏话使用热力图,以及D3中的自定义形状、路径操作、SVG的导入导出和刷子选择等功能。这些功能为我们提供了丰富的工具来创建各种复杂而有趣的数据可视化。在实际应用中,我们可以根据具体需求选择合适的可视化方法,并通过调整参数和插值器来实现不同的可视化效果。例如,在创建Voronoi图时,我们可以调整随机点的数量和颜色插值器;在创建热力图时,我们可以调整时间间隔和颜色桶的数量。通过不断尝试和实践,我们可以更好地掌握D3的强大功能,为数据可视化带来更多的创意和可能性。

以下是本文涉及的主要功能和相关代码的总结表格:
| 功能 | 代码示例 |
| — | — |
| 嵌套Voronoi图 | 见章节1中的相关代码 |
| 电影脏话使用热力图 | 见章节2中的相关代码 |
| D3符号 | 见章节3.1中的代码 |
| 直接构建路径 | 见章节3.2中的代码 |
| SVG导出 | 见章节3.3中的代码 |
| SVG导入 | 见章节3.4中的代码 |
| 刷子选择 | 见章节3.5中的代码 |

通过这些功能和代码示例,我们可以在数据可视化的道路上迈出更坚实的步伐。

基于粒子群优化算法的p-Hub选址优化(Matlab代码实现)内容概要:本文介绍了基于粒子群优化算法(PSO)的p-Hub选址优化问题的研究实现,重点利用Matlab进行算法编程和仿真。p-Hub选址是物流交通网络中的关键问题,旨在通过确定最优的枢纽节点位置和非枢纽节点的分配方式,最小化网络总成本。文章详细阐述了粒子群算法的基本原理及其在解决组合优化问题中的适应性改进,结合p-Hub中转网络的特点构建数学模型,并通过Matlab代码实现算法流程,包括初始化、适应度计算、粒子更新收敛判断等环节。同时可能涉及对算法参数设置、收敛性能及不同规模案例的仿真结果分析,以验证方法的有效性和鲁棒性。; 适合人群:具备一定Matlab编程基础和优化算法理论知识的高校研究生、科研人员及从事物流网络规划、交通系统设计等相关领域的工程技术人员。; 使用场景及目标:①解决物流、航空、通信等网络中的枢纽选址路径优化问题;②学习并掌握粒子群算法在复杂组合优化问题中的建模实现方法;③为相关科研项目或实际工程应用提供算法支持代码参考。; 阅读建议:建议读者结合Matlab代码逐段理解算法实现逻辑,重点关注目标函数建模、粒子编码方式及约束处理策略,并尝试调整参数或拓展模型以加深对算法性能的理解。
内容概要:本文全面介绍了C#全栈开发的学习路径资源体系,涵盖从基础语法到企业级实战的完整知识链条。内容包括C#官方交互式教程、开发环境搭建(Visual Studio、VS Code、Mono等),以及针对不同应用场景(如控制台、桌面、Web后端、跨平台、游戏、AI)的进阶学习指南。通过多个实战案例——如Windows Forms记事本、WPF学生管理系统、.NET MAUI跨平台动物鉴、ASP.NET Core实时聊天系统及Unity 3D游戏项目——帮助开发者掌握核心技术栈架构设计。同时列举了Stack Overflow、Power BI、王者荣耀后端等企业级应用案例,展示C#在高性能场景下的实际运用,并提供了高星开源项目(如SignalR、AutoMapper、Dapper)、生态工具链及一站式学习资源包,助力系统化学习工程实践。; 适合人群:具备一定编程基础,工作1-3年的研发人员,尤其是希望转型全栈或深耕C#技术栈的开发者; 使用场景及目标:①系统掌握C#在不同领域的应用技术栈;②通过真实项目理解分层架构、MVVM、实时通信、异步处理等核心设计思想;③对接企业级开发标准,提升工程能力和实战水平; 阅读建议:此资源以开发简化版Spring学习其原理和内核,不仅是代码编写实现也更注重内容上的需求分析和方案设计,所以在学习的过程要结合这些内容一起来实践,并调试对应的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值