原文:
towardsdatascience.com/death-in-the-himalayas-cdb931228ca7
学习 D3
我一直想学习 D3。说实话,D3 对我来说一直是过度杀鸡用牛刀(在我处理的问题中,可视化数据只是达到目的的手段,而不是最终产品本身)。作为一名 Python 开发者,我经常使用像matplotlib、plotly、seaborn、pandas(或geopandas)和bokeh这样的工具“完成任务”。然而,最近,我一直在花时间只是为了乐趣而创建数据可视化,这似乎是开始学习 D3 的完美时机。
在这篇文章中,我将向您展示我是如何使用 Python、D3 和 Illustrator 创建像上面那样的 5 座山峰(珠穆朗玛峰、阿玛达布拉姆、珠峰、洛子峰和马纳斯鲁峰)的图形的。我将介绍:
-
灵感。
-
获取数据。
-
初始数据准备。
-
选择 5 座山峰进行可视化。
-
准备绘图数据。
-
使用 D3 创建 SVG。
-
保存 SVG 并将其导入 Illustrator。
-
在 Illustrator 中处理 SVG。
-
添加最终修饰。
-
得到的教训。
1. 灵感
这个可视化灵感来源于由Barbara Rebolledo创作的"Gisa 的时序图"。我一直在寻找一种非标准的方式来可视化喜马拉雅探险中的死亡人数,觉得 Barbara 的时序图看起来很有趣(这提供了一个完美的借口来使用 D3,因为用 Python 创建类似的东西将会是一场噩梦)。
2. 获取数据
我使用的数据来自Himalayan Database,与我在文章"可视化珠穆朗玛峰探险"中使用的数据集相同。
Himalayan Database 是所有在尼泊尔喜马拉雅山脉攀登的探险记录的汇编。
具体来说,我通过遵循 Himalayan Database网站上的说明来提取喜马拉雅探险的信息。数据集是一个包含探险记录的小 CSV 文件(略少于 11,200 行)。以下是前 5 行:
expid peakid year season host route1 route2 route3 route4 nation leaders sponsor success1 success2 success3 success4 ascent1 ascent2 ascent3 ascent4 claimed disputed countries approach bcdate smtdate smttime smtdays totdays termdate termreason termnote highpoint traverse ski parapente camps rope totmembers smtmembers mdeaths tothired smthired hdeaths nohired o2used o2none o2climb o2descent o2sleep o2medical o2taken o2unkwn othersmts campsites accidents achievment agency comrte stdrte primrte primmem primref primid chksum
0 ANN260101 ANN2 1960 1 1 NW Ridge-W Ridge NaN NaN NaN UK J. O. M. Roberts NaN True False False False 1st NaN NaN NaN False False India, Nepal Marshyangdi->Hongde->Sabje Khola 3/15/60 5/17/60 1530.0 63 0 - - 1 NaN 7937 False False False 6 0 10 2 0 9 1 0 False True False True False True False False False Climbed Annapurna IV (ANN4-601-01) BC(15/03,3350m),ABC(4575m),C1(5365m),C2(5800m)... NaN NaN NaN False False False False False NaN 2442047
1 ANN269301 ANN2 1969 3 1 NW Ridge-W Ridge NaN NaN NaN Yugoslavia Ales Kunaver Mountaineering Club of Slovenia True False False False 2nd NaN NaN NaN False False NaN Marshyangdi->Hongde->Sabje Khola 9/25/69 10/22/69 1800.0 27 31 10/26/69 1 NaN 7937 False False False 6 0 10 2 0 0 0 0 False False True False False False False False False Climbed Annapurna IV (ANN4-693-02) LowBC(25/09,3950m),BC(27/09,4650m),C1(27/09,53... Draslar frostbitten hands and feet NaN NaN False False False False False NaN 2445501
2 ANN273101 ANN2 1973 1 1 W Ridge-N Face NaN NaN NaN Japan Yukio Shimamura Sangaku Doshikai Annapurna II Expedition 1973 True False False False 3rd NaN NaN NaN False False NaN Marshyangdi->Pisang->Salatang Khola 3/16/73 5/6/73 2030.0 51 0 - - 1 NaN 7937 False False False 5 0 6 1 0 8 0 0 False False True False False False False False False NaN BC(16/03,3300m),C1(21/03,4200m),C2(10/04,5000m... NaN NaN NaN False False False False False NaN 2446797
3 ANN278301 ANN2 1978 3 1 N Face-W Ridge NaN NaN NaN UK Richard J. Isherwood British Annapurna II Expedition False False False False NaN NaN NaN NaN False False NaN Marshyangdi->Pisang->Salatang Khola 9/8/78 10/2/78 NaN 24 27 10/5/78 4 Abandoned at 7000m (on A-IV) due to bad weather 7000 False False False 0 0 2 0 0 0 0 0 True False True False False False False False False NaN BC(08/09,5190m),xxx(02/10,7000m) NaN NaN NaN False False False False False NaN 2448822
4 ANN279301 ANN2 1979 3 1 N Face-W Ridge NW Ridge of A-IV NaN NaN UK Paul Moores NaN False False False False NaN NaN NaN NaN False False NaN Pokhara->Marshyangdi->Pisang->Sabje Khola - - 10/18/79 NaN 0 0 10/20/79 4 Abandoned at 7160m due to high winds 7160 False False False 0 0 3 0 0 0 0 0 True False True False False False False False False NaN BC(3500m),ABC,Biv1,Biv2,Biv3,Biv4,Biv5,xxx(18/... NaN NaN NaN False False False False False NaN 2449204
3. 初始数据准备
我想展示随着时间的推移死亡人数的变化,如果可能的话,我还想添加关于登顶成功率和死亡率的信息(这些最终在最终可视化中只是小点)。我决定专注于以下列:
-
year– 探险是在什么时候进行的? -
expid— 探险 ID(与year结合时是唯一的)。 -
peakid– 山峰 ID。 -
totmembers + tothired– 探险中的成员数。 -
smtmembers + smthired– 登顶的成员数。 -
mdeaths + hdeaths– 死亡的成员数。
包含expid是有用的,因为它有助于我们在需要澄清时获取有关探险的额外信息。例如,有一些成员数为 0 的探险。我假设这是一个错误,但这也可能意味着这些探险根本不是探险,而是代表其他形式的记录。我们可以通过查找一些这些探险在线来确认我们的直觉是否正确。以探险ANNS7130为例。这个探险似乎有 0 名成员。然而,喜马拉雅数据库在线显示,恰好有一名成员:Tomoyo Minegishi。
探险只有一名成员。
“Total Mbrs”字段中列出的成员数为 0。
很明显,数据集存在问题,我决定从分析中删除这些记录(成员数为 0 的探险)。
在删除NaN后,按year和peakid分组,计算成员总数、登顶次数和死亡人数,并删除任何成员数为 0 的(year, peakid)组合(只有 23 个这样的组合),这就是数据(exp_df)的外观:
>>> exp_df
year peakid no_summits no_members no_deaths no_exped
0 1905 KANG 0 9 5 1
1 1907 KABN 0 2 0 1
2 1909 JONG 0 1 0 1
3 1909 LNPO 1 1 0 1
4 1910 KANG 0 1 0 1
-
year和peakid的定义与之前相同。 -
no_members是成员数。 -
no_summits是登顶的成员数。 -
no_deaths是成员死亡数。 -
no_exped是探险次数。
数据库中剩下 406 个山峰。我认为只选择其中几个对我来说想要创建的图表类型来说是最合适的。
4. 选择 5 个山峰进行可视化
观察每个山峰的探险次数,我发现只有少数山峰自 1905 年以来包含了大部分探险:
>>> key_exp = exp_df.groupby(by='peakid')[['no_exped']].sum().reset_index()
>>> key_exp.no_exped.describe()
mean 27.485222
min 1.000000
25% 2.000000
50% 3.000000
75% 8.000000
max 2303.000000
自 1905 年以来,75%的山峰探险次数少于 8 次!(至少根据喜马拉雅数据库)。例如,这里是从 1905 年以来只有一次探险的 10 个山峰:
>>> key_exp.tail(10)
peakid no_exped
252 NALS 1 # Nalakankar South
68 DHEC 1 # Dhechyan Khang
186 KUML 1 # Khumbutse
343 SAUL 1 # Saula
342 SATO 1 # Sat Peak
71 DOGA 1 # Dogari
340 SANK 1 # Sano Kailash
254 NAN2 1 # Nangamari II
75 DOR2 1 # Dorje Lakpa II
129 HMLE 1 # Himlung East
我决定专注于拥有最多探险的 5 个山峰:珠穆朗玛峰、阿玛达布拉姆峰、珠峰、马纳斯鲁峰和洛子峰。
>>> key_exp.sort_values(by='no_exped', inplace=True, ascending=False, ignore_index=False)
>>> key_exp.iloc[:5, :]
peakid no_exped
84 EVER 2303 # Everest
1 AMAD 1525 # Ama Dablam
45 CHOY 1350 # Cho Oyu
233 MANA 754 # Manaslu
210 LHOT 497 # Lhotse
5. 准备绘图数据
为了在绘图时使我的生活更轻松,我对 DataFrame 做了一些修改。
为 5 个选定的山峰创建所有年份/山峰组合
我选择删除 1921 年之前的年份,因为在那之前这 5 个山峰没有探险。在将这些所有(year, peak)组合添加到这些山峰后,我们将引入一些NaN值,这些值可以用 0 来替换:
year peakid no_summits no_members no_deaths no_exped
0 1921 AMAD NaN NaN NaN NaN
1 1921 CHOY NaN NaN NaN NaN
2 1921 EVER 0.0 30.0 2.0 1.0
3 1921 LHOT NaN NaN NaN NaN
4 1921 MANA NaN NaN NaN NaN
到目前为止,我们的数据集是一个包含 500 多行的 DataFrame。
**注意:**添加所有year和peakid组合不是严格必要的,但我不确定是否想在可视化中包含没有探险的年份和山峰。我决定先保留所有组合,并在看到可视化后再做决定。
添加"是否为好季节"标志
我添加了一个名为is_good_seas(“seas"代表"季节”)的列,其值将在(year, peak)组合至少有一个探险但没有死亡(即是一个"好"季节)时设置为True:
year peakid no_summits no_members no_deaths no_exped is_good_seas
510 2023 AMAD 27.0 126.0 0.0 8.0 True
511 2023 CHOY 5.0 9.0 0.0 1.0 True
512 2023 EVER 677.0 1251.0 18.0 50.0 False
513 2023 LHOT 107.0 153.0 0.0 20.0 True
514 2023 MANA 8.0 44.0 0.0 5.0 True
添加"死亡率"和"成功率"列
"成功率"定义为succrate = no_summits / no_members,而"死亡率"简单地定义为deathrate = no_deaths / no_members。我将这些比率作为新列添加到 DataFrame 中,还包括两个其他列:一个标记死亡率高于 10%的列,另一个标记成功率高于 70%的列(这些数字是在创建可视化初稿后确定的)。在这个阶段,DataFrame 看起来是这样的:
year peakid no_summits no_members no_deaths no_exped is_good_seas deathrate high_deathrate succrate high_succrate
200 1961 AMAD 4.0 5.0 0.0 1.0 True 0.000000 False 0.800000 True
450 2011 AMAD 284.0 402.0 1.0 79.0 False 0.002488 False 0.706468 True
466 2014 CHOY 231.0 328.0 0.0 45.0 True 0.000000 False 0.704268 True
477 2016 EVER 678.0 935.0 5.0 80.0 False 0.005348 False 0.725134 True
481 2017 CHOY 77.0 105.0 0.0 6.0 True 0.000000 False 0.733333 True
删除不必要的列,排序并添加时间索引
删除不必要的列不是严格必要的,但有助于保持整洁。为此,我删除了no_summits、no_members、succrate和deathrate列。我还按year和peakid(升序)排序,并为每个peakid添加了一个时间索引(idx):
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx
0 1921 AMAD 0.0 0.0 False False False 0
1 1922 AMAD 0.0 0.0 False False False 1
2 1923 AMAD 0.0 0.0 False False False 2
3 1924 AMAD 0.0 0.0 False False False 3
4 1925 AMAD 0.0 0.0 False False False 4
idx列与year列具有相同的概念性作用,但我认为在绘图时可能有用。
最后,我决定删除没有任何探险的记录(这是我在创建第一个绘图草稿后决定做的,当时我意识到包括这些记录会使图表看起来过于杂乱)。过滤后,我删除了no_exped列:
year peakid no_deaths is_good_seas high_deathrate high_succrate idx
37 1958 AMAD 0.0 True False False 37
38 1959 AMAD 2.0 False True False 38
40 1961 AMAD 0.0 True False True 40
57 1978 AMAD 0.0 True False False 57
58 1979 AMAD 1.0 False False False 58
对no_deaths进行对数变换和归一化
我想让图中每个正方形的厚度代表相对于每个其他year和peak组合的死亡人数。换句话说,我想让探险次数较少(因此死亡人数较少)的峰值由较薄的方块组成,而探险次数较多(因此死亡人数较多)的峰值由较厚的方块组成。这意味着珠穆朗玛峰的图将由漂亮的厚方块组成,但其他 4 个峰值的图将由薄(几乎看不见)的方块组成。为了解决这个问题,我决定对所有 5 个山峰和年份的死亡人数进行对数变换。我还将(对数变换后的)非零值归一化到区间[0.5, 3]内(因为这个值打算用作绘图时的线宽)。
在对数变换和归一化后,我得到了类似这样的结果:
year peakid no_deaths is_good_seas high_deathrate high_succrate idx
37 1958 AMAD 0.000000 True False False 37
38 1959 AMAD 1.099531 False True False 38
40 1961 AMAD 0.000000 True False True 40
57 1978 AMAD 0.000000 True False False 57
58 1979 AMAD 0.500000 False False False 58
no_deaths列的值位于区间[0.5, 3]内,或者等于 0。
添加峰值名称并分割成 5 个 CSV 文件
喜马拉雅数据库有一个将peakid映射到山峰名称的表,我已经将其合并到 DataFrame 中:
year peakid no_deaths is_good_seas high_deathrate high_succrate idx pkname
0 1958 AMAD 0.000000 True False False 37 Ama Dablam
1 1959 AMAD 1.099531 False True False 38 Ama Dablam
2 1961 AMAD 0.000000 True False True 40 Ama Dablam
3 1978 AMAD 0.000000 True False False 57 Ama Dablam
4 1979 AMAD 0.500000 False False False 58 Ama Dablam
我接着将数据分成 5 个 CSV 文件:ama_dablam.csv、cho_oyu.csv、everest.csv、lhotse.csv 和 manaslu.csv,每个山峰一个:
# ama_dablam.csv (49 rows)
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx pkname
0 1958 AMAD 0.000000 1.0 True False False 37 Ama Dablam
1 1959 AMAD 1.099531 1.0 False True False 38 Ama Dablam
2 1961 AMAD 0.000000 1.0 True False True 40 Ama Dablam
3 1978 AMAD 0.000000 1.0 True False False 57 Ama Dablam
4 1979 AMAD 0.500000 4.0 False False False 58 Ama Dablam
# cho_oyu.csv (53 rows)
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx pkname
49 1951 CHOY 0.000000 1.0 True False False 30 Cho Oyu
50 1952 CHOY 0.000000 1.0 True False False 31 Cho Oyu
51 1954 CHOY 0.000000 2.0 True False False 33 Cho Oyu
52 1958 CHOY 0.500000 1.0 False False False 37 Cho Oyu
53 1959 CHOY 1.699062 1.0 False True False 38 Cho Oyu
# everest.csv (76 rows)
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx pkname
102 1921 EVER 1.099531 1.0 False False False 0 Everest
103 1922 EVER 2.183097 1.0 False True False 1 Everest
104 1924 EVER 1.699062 1.0 False False False 3 Everest
105 1933 EVER 0.000000 1.0 True False False 12 Everest
106 1934 EVER 0.500000 1.0 False True False 13 Everest
# lhotse.csv (52 rows)
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx pkname
178 1955 LHOT 0.0 1.0 True False False 34 Lhotse
179 1956 LHOT 0.0 1.0 True False False 35 Lhotse
180 1972 LHOT 0.0 1.0 True False False 51 Lhotse
181 1973 LHOT 0.0 1.0 True False False 52 Lhotse
182 1974 LHOT 0.5 2.0 False False False 53 Lhotse
# manaslu.csv (60 rows)
year peakid no_deaths no_exped is_good_seas high_deathrate high_succrate idx pkname
230 1950 MANA 0.0 1.0 True False False 29 Manaslu
231 1952 MANA 0.0 1.0 True False False 31 Manaslu
232 1953 MANA 0.0 1.0 True False False 32 Manaslu
233 1954 MANA 0.0 1.0 True False False 33 Manaslu
234 1955 MANA 0.0 1.0 True False False 34 Manaslu
6. 使用 D3 创建 SVG
我决定为每个山峰分别创建一个图表。为了具体化,让我们假设我正在创建 Manaslu 的图表(代码将通过简单地更改 CSV 文件的数据路径来为其他山峰重用)。
基本设置
我首先创建了一个名为 index.html 的基本 HTML 文件(位于上面创建的 CSV 文件相同的文件夹中),其中包含 D3 库:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Squares With D3</title>
<!-- Include D3 library -->
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
</body>
</html>
接下来,我创建了一个 SVG 容器(你可以将其视为绘制视觉元素的画布),并在 <script> 标签中开始,我唯一做的事情就是通过其 id 选择容器:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Squares With D3</title>
<!-- Include D3 library -->
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<!-- Create SVG container -->
<svg width="2000" height="500" id="visualization"></svg>
<script>
// Select the SVG container by id
const svg = d3.select("#visualization");
</script>
</body>
</html>
如果你在一个网页浏览器中打开 index.html,你应该看到一个空白页面。
添加背景颜色
添加背景颜色很简单:只需绘制一个具有所需颜色的矩形,并确保它覆盖整个 SVG。具体来说(省略 <body> 标签之外的所有内容):
<body>
<!-- Create SVG container -->
<svg width="2000" height="500" id="visualization"></svg>
<script>
// Select the SVG container by id
const svg = d3.select("#visualization");
// Define constants
const backgroundColor = "rgba(243, 243, 243, 1)";
// Add a background with the desired color
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", backgroundColor);
</script>
</body>
如果你在一个网页浏览器中打开 index.html,你应该看到这个。
现在我们准备开始向我们的 SVG 添加一些数据。
添加山峰名称
让我们在 SVG 的左上角添加山峰的名称。首先,定义一些常量:
-
x0和y0:用于指定放置山峰名称的位置。 -
blackColor:用于指定文字颜色。
然后,加载 CSV 文件,将山峰名称存储为名为 peakName 的变量,并将其添加到 SVG 中:
<body>
<!-- Create SVG container -->
<svg width="2000" height="500" id="visualization"></svg>
<script>
// Select the SVG container
const svg = d3.select("#visualization");
// Constants
const backgroundColor = "rgba(243, 243, 243, 1)";
const blackColor = "rgba(22, 22, 22, 1)";
const x0 = 100;
const y0 = 100;
// Add a background square with the desired color
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", backgroundColor);
// Load the CSV file (assuming in same directory)
d3.csv("manaslu.csv").then(data => {
// Store the peak name
peakName = data[0].pkname;
// Add text with peak name to SVG
svg.append("text")
.attr("x", x0)
.attr("y", y0 - 40)
.text(`Peak: ${peakName}`)
.style("fill", blackColor)
.style("font-size", "14px")
.style("font-weight", "bold");
});
</script>
</body>
SVG 现在应该看起来像这样:
重要提示: 如果你的图表突然变空白,可能是由于 CORS 策略阻止而无法加载 manaslu.csv。如果发生这种情况,打开终端并启动一个简单的 HTTP 服务器(你可以通过输入 python3 -m http.server 来完成此操作),然后在浏览器中打开 localhost:8000/,导航到 index.html 文件,并打开它。
接下来,我们将绘制正方形。
绘制正方形的逻辑
我想让每个正方形都是一个闭合路径。路径将由两条垂直线和两条对角线组成。从 1921 年开始,我会决定是否为那个 year(idx = 0)绘制一个正方形,使用以下逻辑:
-
绘制红色正方形,如果那年有死亡事件。线条的粗细应由
no_deaths列确定。 -
如果那年有探险但没有死亡(黑色正方形代表“好”季节),则绘制一个线条粗细为 0.25 的黑色正方形。
-
对于没有探险的年份(这些年份已从 DataFrame 中删除,因此强制执行此要求很简单)不绘制任何内容。
-
然后,向右移动一步,继续到下一个
year(idx = 1)。 -
重复。
绘制正方形
我首先定义了一些额外的常量来指定线长、对角线的角度、向右移动的步长以及我想要使用的特定红色(省略了<script>标签之外的所有内容):
<script>
// Select the SVG container
const svg = d3.select("#visualization");
// Constants
const backgroundColor = "rgba(243, 243, 243, 1)";
const blackColor = "rgba(22, 22, 22, 1)";
const x0 = 100;
const y0 = 100;
const redColor = "rgba(240, 52, 52, 1)";
const vert_len = 150; // Length of vertical lines
const diag_len = 100; // Length of diagonal lines
const angle = 135; // Angle of diagonal lines
const translationStep = 12; // How big of a step to take between years
// Define colors for each square based on is_good_season
const seasonColors = {
true: blackColor,
false: redColor,
};
// Add a background square with the desired color
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", backgroundColor);
// Load the CSV file (assuming in same directory)
d3.csv("manaslu.csv").then(data => {
peakName = data[0].pkname;
// Add text with peak name at the top left
svg.append("text")
.attr("x", x0)
.attr("y", y0 - 40)
.text(`Peak: ${peakName}`)
.style("fill", "black")
.style("font-size", "14px")
.style("font-weight", "bold");
});
</script>
接下来,遍历data中的每一行(记住,行已经按year/idx升序排序)并使用上一节中的逻辑绘制一个方块。这可以通过添加以下代码来完成:
// Add this after the code for adding peak name
// and sitll inside d3.csv("manaslu.csv").then(data => {})
// Iterate through each row in the data for squares
data.forEach(row => {
// Extract is_good_seas from CSV and convert to boolean
const isGoodSeason = row.is_good_seas === "True";
// Calculate key values for square coordinates
const x = x0 + row.idx * translationStep;
const x2 = x + diag_len * Math.cos((-angle) * (Math.PI / 180));
const y2 = y0 + vert_len + diag_len * Math.sin(angle * (Math.PI / 180));
// Draw square
svg.append("path")
.attr("d", d3.line().curve(d3.curveLinearClosed)([
[x, y0],
[x, y0 + vert_len],
[x2, y2],
[x2, y2 - vert_len],
]))
.style("stroke", seasonColors[isGoodSeason])
.style("stroke-width", isGoodSeason ? 0.25 : row.no_deaths)
.style("fill", backgroundColor)
});
这是我们目前拥有的 SVG:
添加红/黑点以标记死亡率和成功率
我们已经完成了红/黑方块的绘制。然而,我想在每个方块下方添加一个小黑点,每当那一年的成功率大于 70%时,以及当那一年的死亡率大于 10%时添加一个小红点。这样做很简单。只需在绘制方块后添加此代码:
// Add this after the code for drawing the squares
// and still inside data.forEach(row => {})
// Check if "high_deathrate" is True, then add a red dot below the square
if (row.high_deathrate === "True") {
svg.append("circle")
.attr("cx", x2)
.attr("cy", y2 + 10)
.attr("r", 2.5)
.style("stroke", redColor)
.style("fill", backgroundColor);
}
// Check if "high_succrate" is True, then add a black dot below the square
const secondCircleOffset = row.high_deathrate === "False" ? 10 : 20;
if (row.high_succrate === "True") {
svg.append("circle")
.attr("cx", x2)
.attr("cy", y2 + secondCircleOffset)
.attr("r", 2.5)
.style("fill", blackColor);
}
注意,我添加了一些逻辑来处理high_succrate == True和high_deathrate == True同时出现的情况。特别是这一行:
row.high_deathrate === "False" ? 10 : 20;
当一个红点已经被绘制时,会移动黑点向下(实际上这种情况从未发生,我没有看到这个动作)。
这就是最终的 SVG 的样子:
到目前为止,我们已经完成了与 D3 的工作。现在我们准备保存我们的 SVG 并在 Illustrator 中使用它。
7. 保存 SVG 并将其导入 Illustrator
在我们能够在 Illustrator 中使用 SVG 之前,我们需要保存它。
保存 SVG
如果你使用的是 Chrome,你可以右键单击你的 SVG 并点击“检查”以打开 Chrome 开发者工具:
那么,在开发者工具的“元素”标签中找到 SVG 元素,右键单击它,然后选择复制 > 复制元素:
接下来,打开一个文本编辑器并将内容粘贴进去。保存文件,并确保使用.svg作为文件扩展名:
manaslu.svg
如果我没有使用 Chrome 怎么办?
其他浏览器有类似的功能。但是,如果这不起作用(无论什么原因),另一个选项是在你的 HTML 文件中添加一个按钮,允许你在点击按钮时下载 SVG。
在 Illustrator 中打开 SVG
如果你将manaslu.svg在 Illustrator 中打开,你可能会看到如下内容:
实际上,我不确定为什么背景是黑色,但将颜色改回应有的颜色很容易(只需三步):
8. 在 Illustrator 中使用 SVG
Adobe Illustrator 是一款强大的矢量图形编辑器,允许用户创建和操作数字艺术作品。与 PowerPoint 等演示软件不同,Illustrator 专门用于图形设计和插画。将 Illustrator 想象成一个数字画布,你可以精确地创建复杂的设计、标志、图标和插画。
我不会详细讲解我遵循的整个 Illustrator 流程,但有一些关键的操作在 Illustrator 中你可以做到,我想强调一下(如果你之前从未使用过 Illustrator,这将给你一个大致的概念)。
锁定对象
我喜欢锁定背景,这样它就不能被移动或修改。只需选择背景,然后转到对象 > 锁定 > 选择。当有绝对不希望被干扰的元素时,这是一个很棒的功能。
分组对象
就像在 PowerPoint 中一样,你可以在 Illustrator 中分组对象。这非常有用,因为它可以帮助你避免不小心独立移动正方形,从而“弄乱数据”。本质上,它有助于防止做类似这样的事情:
如果没有对正方形和点进行分组,很容易不小心做出这样的事情。技术上即使你将对象分组,这也仍然可能做到,所以分组是有帮助的,但并不能完全防止不小心弄乱数据。在 Illustrator 中编辑 SVG 时,这一点很重要,需要小心处理。
选择相似对象
假设我想将所有正方形的填充透明度更改为 20%,但我不想影响轮廓的透明度。Illustrator 使这一操作变得非常简单。实现这种效果的一种方法是从一个正方形开始选择,然后转到选择 > 相同 > 填充颜色。这将选择所有具有相同填充颜色的对象。然后你可以从外观面板中编辑填充颜色的透明度:
9. 最终调整
我对纹理情有独钟,所以我决定打开 Illustrator 文件并添加纸张纹理。基本步骤如下:
-
打开 Illustrator 文件。
-
下载纹理的图片(Unsplash有很多免费选项)。
-
将纹理转换为黑白,并调整亮度和对比度以提取纹理。
-
将纹理拖到 Illustrator 中的图像上方。
-
将“透明度”模式更改为达到所需的效果。
**(左)由 Kiwihug 在 Unsplash 提供的原始纹理。(右)**去饱和纹理并隔离所需的纹理。
导出
因为我要在网上分享最终图像,并且图像包含带有透明度的颜色,所以我决定将图像导出为 PNG 格式。我选择了“类型优化”抗锯齿设置,以帮助保持文本的清晰度。
这是 Illustrator 中直接输出的设计样子:
最终图像
这是珠穆朗玛峰最终图像的样子:
如果您感兴趣,5 张最终图像可在我的 网站 上找到。
10. 经验教训
不适合色盲友好
我与一位朋友分享了最终的可视化,很快就被提醒他们有颜色视觉缺陷(CVP)!这可能是他们看到可视化时的样子(取决于 CVP 类型):
CVP 类型:红绿色盲。
CVP 类型:绿蓝色盲。
事后看来,我应该选择不同的调色板。Adobe Color(Adobe Color)提供了构建对 CVP 用户可访问的调色板的出色工具:
Adobe Color 网站展示了不同类型的 CVP 用户看到的调色板是什么样的,并突出了潜在问题。
在低亮度界面上编辑
在过去,我通过艰难的方式学到了以下价值:
-
一台校准良好的显示器。
-
能够精确控制亮度(以保持一致性)。
不幸的是,我没有在 Illustrator 中查看具有较浅界面背景的最终图形。这会让我在导出之前发现图像有点暗。
当与深色背景对比时,事物往往看起来更亮。
最终评论
-
使用 D3 创建初始 SVG 比直接在 Python 中创建此类图表要简单得多。
-
如果你对在 Adobe Illustrator 中编辑基于数据的图形感兴趣,乔纳森·索曼的[视频《在 Adobe Illustrator 中清理 Python 数据可视化(pandas 到 ai2html)》(https://www.youtube.com/watch?v=yPOLDdEMgHo&list=PLQM105guaa4WoOVZ8tVbkwfUrjj5xa4Z2&index=6&t=1153s)似乎涵盖了与编辑 Illustrator 中的数据图形相关的大量重要观点。
-
整个代码可以在这个 GitHub 仓库中找到(可能与这里展示的略有不同)。
320

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



