D3.js与和弦图
D3.js
D3.js(Data-Driven Documents)是一个开源的JavaScript库,用于使用Web标准来生成丰富的交互式数据可视化的工具。D3.js结合了强大的可视化组件和数据驱动的DOM操作方法,让开发者能够自由地将数据绑定到文档对象模型(DOM),然后通过数据来驱动DOM的变换和动画。
D3.js文档地址:D3.js
和弦图
和弦图(Chord Diagram)是一种用于展示数据矩阵中各元素之间关系的可视化图形。它以圆形布局呈现,其中每个实体(或类别)表示为圆周上的一个弧段,而实体之间的关系则通过连接这些弧段的带状或弦来表示。弦的宽度通常与所表示的关系强度成比例。
一般单流向的常用桑基图,而双流向的利用和弦图展示。
获取指定区域土地利用tif文件
这里我利用GEE平台获取指定shp区域的土地利用并导出tif文件,数据集是MODIS/006/MCD12Q1,代码如下:
// 加载shp文件
var region = ee.FeatureCollection("projects/ee-pengbr/assets/Kongque10kmbuffer");// 加载MODIS土地利用数据
var landcover = ee.ImageCollection("MODIS/006/MCD12Q1").filter(ee.Filter.date('2001-01-01', '2001-12-31')).first().select('LC_Type1'); // 选择特定波段;// 裁剪到shp区域
var landcover_clipped = landcover.clip(region);// 可视化
var visParams = {min: 1,max: 17,palette: ['05450a', '086a10', '54a708', '78d203', '009900', 'c6b044', 'dcd159','dade48', 'fbff13', 'b6ff05', '27ff87', 'c24f44', 'a5a5a5', 'ff6d4c','69fff8', 'f9ffa4', '1c0dff']
};Map.centerObject(region);
Map.addLayer(landcover_clipped, visParams, "Land Cover 2020");// 导出数据
Export.image.toDrive({image: landcover_clipped,description: 'LandCover_2001',folder: 'GEE',region: region.geometry(),scale: 250,crs: 'EPSG:4326',maxPixels: 1e13
});
利用这段代码获取了指定shp区域2001年和2020年的土地利用tif文件
这是MODIS/006/MCD12Q1数据集土地利用分类的部分介绍,不同的像元值代表不同的土地利用类型
分析像元值的变化
有了tif文件,现在需要分析2001年到2020年tif文件中逐个像元值的变化,从而得出土地利用类型的变化,下面使用python实现这一步骤
这段代码首先同时顺序读取两个tif文件的像元,并使用元组的方式(tif1像元1,tif2像元1)存入列表中(如(1,6),表示土地利用类型从1变到了6),最后使用Couter计算有多少种组合类别((1,6)算一种)和每一类别的个数((1,6)有多少个)
import rasterio
from collections import Counter# 文件路径
tif1_path = 'LandCover_2001.tif'
tif2_path = 'LandCover_2020.tif'# 初始化组合统计
pixel_value_change = []# 打开tif文件并读取像元值
with rasterio.open(tif1_path) as tif1, rasterio.open(tif2_path) as tif2:# 检查文件尺寸是否一致if tif1.width != tif2.width or tif1.height != tif2.height:raise ValueError('两个tif文件的尺寸不一致,无法逐像元比较。')# 读取栅格数据data1 = tif1.read(1) # 第一个文件的第一波段data2 = tif2.read(1) # 第二个文件的第一波段# 收集所有像元组合for i in range(data1.shape[0]): # 遍历行for j in range(data1.shape[1]): # 遍历列pixel_value_change.append((data1[i, j], data2[i, j])) # 使用元组存储组合# 使用 Counter 统计每种组合的个数
combination_counts = Counter(pixel_value_change)# 输出结果
print(f"总共有 {len(combination_counts)} 种不同的组合。")
print("每种组合及其个数如下:")
for combination, count in combination_counts.items():print(f"组合 {combination} 的个数为 {count}")
输出结果如下:
我们发现有的像元土地利用没有发生变化,即自流向。同时也有双流向,即从10到12和12到10,所有这里使用和弦图绘制土地利用变化较为合适,而不使用桑基图。
总共有 20 种不同的组合。
每种组合及其个数如下:
组合 (0, 0) 的个数为 1224272 # 这个0值是shp范围外的数据,可以不用考虑
组合 (12, 12) 的个数为 18293
组合 (10, 12) 的个数为 11696
组合 (12, 10) 的个数为 1127
组合 (10, 10) 的个数为 16449
组合 (13, 13) 的个数为 2795
组合 (16, 10) 的个数为 7660
组合 (16, 16) 的个数为 174953
组合 (16, 11) 的个数为 32
组合 (10, 11) 的个数为 205
组合 (11, 11) 的个数为 589
组合 (10, 16) 的个数为 4388
组合 (11, 10) 的个数为 474
组合 (11, 16) 的个数为 9
组合 (11, 12) 的个数为 14
组合 (17, 10) 的个数为 13
组合 (16, 12) 的个数为 4584
组合 (16, 7) 的个数为 10
组合 (12, 16) 的个数为 10
组合 (16, 15) 的个数为 595
D3.js绘制土地利用变化和弦图
以下代码,主要就是流向矩阵matrix需要详细解释
假设有以下节点顺序:
• 第 0 个节点:crop
• 第 1 个节点:grassland
• 第 2 个节点:city
• 第 3 个节点:barren
• 第 4 个节点:wetland
• 第 5 个节点:water
• 第 6 个节点:shrub
• 第 7 个节点:ice
在matrix中,每一行的数据表示从该节点流向其他节点的数量。例如:
第 0 行(crop 流出到其他节点)这些数据来自于上面的组合个数
[18293, 11696, 0, 0, 0, 0, 0, 0], // crop
• 18293: 从 crop 自身流向 crop 的量。
• 11696: 从 crop 流向 grassland 的量。
• 0: 从 crop 流向 city 的量。
• 其他值为 0,表示 crop 没有流向其他节点。
第 1 行(grassland 流出到其他节点)
[1127, 16449, 0, 4388, 205, 0, 0, 0] // grassland
• 1127: 从 grassland 流向 crop 的量。
• 16449: 从 grassland 流向 grassland 的量。
• 4388: 从 grassland 流向 barren 的量。
• 205: 从 grassland 流向 wetland 的量。
其它行依次类推
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>D3 Chord Diagram</title><script src="https://d3js.org/d3.v7.min.js"></script><style>.chord path {fill-opacity: 0.7;stroke: #000;stroke-width: 0.5px;}.group text {font: 16px sans-serif;}.group path {fill: none;stroke: #000;}</style>
</head>
<body>
<svg id="chord" width="800" height="800"></svg><script>const width = 800, height = 800, outerRadius = Math.min(width, height) / 2 - 40;const innerRadius = outerRadius - 30;const matrix = [[18293, 11696, 0, 0, 0, 0, 0, 0], // crop[1127, 16449, 0, 4388, 205, 0, 0, 0], // grassland[0, 0, 2795, 0, 0, 0, 0, 0], // city[4584, 7660, 0, 174953, 32, 0, 10, 595], // barren[14, 474, 0, 9, 589, 0, 0, 0], // wetland[0, 13, 0, 0, 0, 0, 0, 0], // water[0, 0, 0, 0, 0, 0, 0, 0], // shrub[0, 0, 0, 0, 0, 0, 0, 0] // ice];const labels = ["crop", "grassland", "city", "barren", "wetland", "water", "shrub", "ice"];const color = d3.scaleOrdinal(d3.schemeCategory10);// Chord diagram layoutconst chord = d3.chord().padAngle(0.05).sortSubgroups(d3.descending);const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);const ribbon = d3.ribbon().radius(innerRadius);const svg = d3.select("#chord").attr("viewBox", [-width / 2, -height / 2 - 40, width, height]);const chords = chord(matrix);// Draw outer arcsconst group = svg.append("g").selectAll("g").data(chords.groups).enter().append("g").attr("class", "group");group.append("path").style("fill", d => color(d.index)).style("stroke", d => d3.rgb(color(d.index)).darker()).attr("d", arc);group.append("text").each(d => {d.angle = (d.startAngle + d.endAngle) / 2;}).attr("dy", "0.35em").attr("transform", d => `rotate(${(d.angle * 180 / Math.PI - 90)})translate(${outerRadius + 10})${d.angle > Math.PI ? "rotate(180)" : ""}`).style("text-anchor", d => d.angle > Math.PI ? "end" : null).text(d => labels[d.index]);// Draw ribbonssvg.append("g").attr("class", "chord").selectAll("path").data(chords).enter().append("path").attr("d", ribbon).style("fill", d => color(d.target.index)).style("stroke", d => d3.rgb(color(d.target.index)).darker());
</script>
</body>
</html>
运行上述代码,最终在浏览器中得到指定区域2001年到2020年土地利用变化和弦图
可以看到,部分荒地变成了耕地和草地,部分草地变成了耕地,其它用地变化不大