数据可视化: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中的代码 |
通过这些功能和代码示例,我们可以在数据可视化的道路上迈出更坚实的步伐。
超级会员免费看
12

被折叠的 条评论
为什么被折叠?



