喜马拉雅山脉的死亡事件

原文:towardsdatascience.com/death-in-the-himalayas-cdb931228ca7

学习 D3

我一直想学习 D3。说实话,D3 对我来说一直是过度杀鸡用牛刀(在我处理的问题中,可视化数据只是达到目的的手段,而不是最终产品本身)。作为一名 Python 开发者,我经常使用像matplotlibplotlyseabornpandas(或geopandas)和bokeh这样的工具“完成任务”。然而,最近,我一直在花时间只是为了乐趣而创建数据可视化,这似乎是开始学习 D3 的完美时机。

在这篇文章中,我将向您展示我是如何使用 Python、D3 和 Illustrator 创建像上面那样的 5 座山峰(珠穆朗玛峰阿玛达布拉姆珠峰洛子峰马纳斯鲁峰)的图形的。我将介绍:

  1. 灵感。

  2. 获取数据。

  3. 初始数据准备。

  4. 选择 5 座山峰进行可视化。

  5. 准备绘图数据。

  6. 使用 D3 创建 SVG。

  7. 保存 SVG 并将其导入 Illustrator。

  8. 在 Illustrator 中处理 SVG。

  9. 添加最终修饰。

  10. 得到的教训。


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

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/fdf244fd681de78b6fa25a88820beab5.png

探险只有一名成员。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1a103b990be15b1cc575bd7e8a549689.png

“Total Mbrs”字段中列出的成员数为 0。

很明显,数据集存在问题,我决定从分析中删除这些记录(成员数为 0 的探险)。

在删除NaN后,按yearpeakid分组,计算成员总数、登顶次数和死亡人数,并删除任何成员数为 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
  • yearpeakid的定义与之前相同。

  • 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。

**注意:**添加所有yearpeakid组合不是严格必要的,但我不确定是否想在可视化中包含没有探险的年份和山峰。我决定先保留所有组合,并在看到可视化后再做决定。

添加"是否为好季节"标志

我添加了一个名为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_summitsno_memberssuccratedeathrate列。我还按yearpeakid(升序)排序,并为每个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进行对数变换和归一化

我想让图中每个正方形的厚度代表相对于每个其他yearpeak组合的死亡人数。换句话说,我想让探险次数较少(因此死亡人数较少)的峰值由较薄的方块组成,而探险次数较多(因此死亡人数较多)的峰值由较厚的方块组成。这意味着珠穆朗玛峰的图将由漂亮的厚方块组成,但其他 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.csvcho_oyu.csveverest.csvlhotse.csvmanaslu.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 选择容器:

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Squares With D3&lt;/title&gt;

    &lt;!-- Include D3 library --&gt;
    &lt;script src="https://d3js.org/d3.v7.min.js"&gt;&lt;/script&gt;

&lt;/head&gt;
&lt;body&gt;

    &lt;!-- Create SVG container --&gt;
    &lt;svg width="2000" height="500" id="visualization"&gt;&lt;/svg&gt;

    &lt;script&gt;

        // Select the SVG container by id
        const svg = d3.select("#visualization");

    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;

如果你在一个网页浏览器中打开 index.html,你应该看到一个空白页面。

添加背景颜色

添加背景颜色很简单:只需绘制一个具有所需颜色的矩形,并确保它覆盖整个 SVG。具体来说(省略 <body> 标签之外的所有内容):

&lt;body&gt;

    &lt;!-- Create SVG container --&gt;
    &lt;svg width="2000" height="500" id="visualization"&gt;&lt;/svg&gt;

    &lt;script&gt;

        // 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);

    &lt;/script&gt;

&lt;/body&gt;

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/42578550cd6c9c22346849a34a349c6f.png

如果你在一个网页浏览器中打开 index.html,你应该看到这个。

现在我们准备开始向我们的 SVG 添加一些数据。

添加山峰名称

让我们在 SVG 的左上角添加山峰的名称。首先,定义一些常量:

  • x0y0:用于指定放置山峰名称的位置。

  • blackColor:用于指定文字颜色。

然后,加载 CSV 文件,将山峰名称存储为名为 peakName 的变量,并将其添加到 SVG 中:

&lt;body&gt;

    &lt;!-- Create SVG container --&gt;
    &lt;svg width="2000" height="500" id="visualization"&gt;&lt;/svg&gt;

    &lt;script&gt;

        // 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 =&gt; {

            // 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");
        });
    &lt;/script&gt;

&lt;/body&gt;

SVG 现在应该看起来像这样:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/457623b7ba27a2b172c260c9f1d8e4c8.png

重要提示: 如果你的图表突然变空白,可能是由于 CORS 策略阻止而无法加载 manaslu.csv。如果发生这种情况,打开终端并启动一个简单的 HTTP 服务器(你可以通过输入 python3 -m http.server 来完成此操作),然后在浏览器中打开 localhost:8000/,导航到 index.html 文件,并打开它。

接下来,我们将绘制正方形。

绘制正方形的逻辑

我想让每个正方形都是一个闭合路径。路径将由两条垂直线和两条对角线组成。从 1921 年开始,我会决定是否为那个 yearidx = 0)绘制一个正方形,使用以下逻辑:

  • 绘制红色正方形,如果那年有死亡事件。线条的粗细应由 no_deaths 列确定。

  • 如果那年有探险但没有死亡(黑色正方形代表“好”季节),则绘制一个线条粗细为 0.25 的黑色正方形。

  • 对于没有探险的年份(这些年份已从 DataFrame 中删除,因此强制执行此要求很简单)不绘制任何内容。

  • 然后,向右移动一步,继续到下一个 yearidx = 1)。

  • 重复。

绘制正方形

我首先定义了一些额外的常量来指定线长、对角线的角度、向右移动的步长以及我想要使用的特定红色(省略了<script>标签之外的所有内容):

&lt;script&gt;

    // 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 =&gt; {

        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");    
    });
&lt;/script&gt;

接下来,遍历data中的每一行(记住,行已经按year/idx升序排序)并使用上一节中的逻辑绘制一个方块。这可以通过添加以下代码来完成:

// Add this after the code for adding peak name
// and sitll inside d3.csv("manaslu.csv").then(data =&gt; {})

// Iterate through each row in the data for squares
data.forEach(row =&gt; {

    // 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:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/061b575e6695354626ce3692e3951b64.png

添加红/黑点以标记死亡率和成功率

我们已经完成了红/黑方块的绘制。然而,我想在每个方块下方添加一个小黑点,每当那一年的成功率大于 70%时,以及当那一年的死亡率大于 10%时添加一个小红点。这样做很简单。只需在绘制方块后添加此代码:

// Add this after the code for drawing the squares
// and still inside data.forEach(row =&gt; {})

// 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 == Truehigh_deathrate == True同时出现的情况。特别是这一行:

row.high_deathrate === "False" ? 10 : 20;

当一个红点已经被绘制时,会移动黑点向下(实际上这种情况从未发生,我没有看到这个动作)。

这就是最终的 SVG 的样子:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/aa57418ce571b153fdadba2f00081667.png

到目前为止,我们已经完成了与 D3 的工作。现在我们准备保存我们的 SVG 并在 Illustrator 中使用它。


7. 保存 SVG 并将其导入 Illustrator

在我们能够在 Illustrator 中使用 SVG 之前,我们需要保存它。

保存 SVG

如果你使用的是 Chrome,你可以右键单击你的 SVG 并点击“检查”以打开 Chrome 开发者工具:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/96c7a0ea492d49092bfc427f9d64ce77.png

那么,在开发者工具的“元素”标签中找到 SVG 元素,右键单击它,然后选择复制 > 复制元素:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/be71ef6e66669beed7fcc3d9d9fb2a2f.png

接下来,打开一个文本编辑器并将内容粘贴进去。保存文件,并确保使用.svg作为文件扩展名:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1ca56a1c3901cc207c4faa88ef64fb7a.png

manaslu.svg

如果我没有使用 Chrome 怎么办?

其他浏览器有类似的功能。但是,如果这不起作用(无论什么原因),另一个选项是在你的 HTML 文件中添加一个按钮,允许你在点击按钮时下载 SVG。

在 Illustrator 中打开 SVG

如果你将manaslu.svg在 Illustrator 中打开,你可能会看到如下内容:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/2831f400d8d08eeeca1cd531a55d032d.png

实际上,我不确定为什么背景是黑色,但将颜色改回应有的颜色很容易(只需三步):

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/db0dccdad6cc870cbee9283d9b608bba.png


8. 在 Illustrator 中使用 SVG

Adobe Illustrator 是一款强大的矢量图形编辑器,允许用户创建和操作数字艺术作品。与 PowerPoint 等演示软件不同,Illustrator 专门用于图形设计和插画。将 Illustrator 想象成一个数字画布,你可以精确地创建复杂的设计、标志、图标和插画。

我不会详细讲解我遵循的整个 Illustrator 流程,但有一些关键的操作在 Illustrator 中你可以做到,我想强调一下(如果你之前从未使用过 Illustrator,这将给你一个大致的概念)。

锁定对象

我喜欢锁定背景,这样它就不能被移动或修改。只需选择背景,然后转到对象 > 锁定 > 选择。当有绝对不希望被干扰的元素时,这是一个很棒的功能。

分组对象

就像在 PowerPoint 中一样,你可以在 Illustrator 中分组对象。这非常有用,因为它可以帮助你避免不小心独立移动正方形,从而“弄乱数据”。本质上,它有助于防止做类似这样的事情:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/35053741cf9303a75af367ca50b06e1c.png

如果没有对正方形和点进行分组,很容易不小心做出这样的事情。技术上即使你将对象分组,这也仍然可能做到,所以分组是有帮助的,但并不能完全防止不小心弄乱数据。在 Illustrator 中编辑 SVG 时,这一点很重要,需要小心处理。

选择相似对象

假设我想将所有正方形的填充透明度更改为 20%,但我不想影响轮廓的透明度。Illustrator 使这一操作变得非常简单。实现这种效果的一种方法是从一个正方形开始选择,然后转到选择 > 相同 > 填充颜色。这将选择所有具有相同填充颜色的对象。然后你可以从外观面板中编辑填充颜色的透明度:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/05b9affc397b6bb1ceae6c56603e6630.png


9. 最终调整

我对纹理情有独钟,所以我决定打开 Illustrator 文件并添加纸张纹理。基本步骤如下:

  1. 打开 Illustrator 文件。

  2. 下载纹理的图片(Unsplash有很多免费选项)。

  3. 将纹理转换为黑白,并调整亮度和对比度以提取纹理。

  4. 将纹理拖到 Illustrator 中的图像上方。

  5. 将“透明度”模式更改为达到所需的效果。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/55956ba926e7f2b192500846239d91f6.png

**(左)KiwihugUnsplash 提供的原始纹理。(右)**去饱和纹理并隔离所需的纹理。

导出

因为我要在网上分享最终图像,并且图像包含带有透明度的颜色,所以我决定将图像导出为 PNG 格式。我选择了“类型优化”抗锯齿设置,以帮助保持文本的清晰度。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/ef62de0f9dd332f5214c978e43e928ca.png

这是 Illustrator 中直接输出的设计样子:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cefcef7440d4bedc4cb06a65b7753e99.png


最终图像

这是珠穆朗玛峰最终图像的样子:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0f3f14418ef22069536da08cce9788ef.png

如果您感兴趣,5 张最终图像可在我的 网站 上找到。


10. 经验教训

不适合色盲友好

我与一位朋友分享了最终的可视化,很快就被提醒他们有颜色视觉缺陷(CVP)!这可能是他们看到可视化时的样子(取决于 CVP 类型):

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9b75285ecd5264ee09c5bb8112ac2324.png

CVP 类型:红绿色盲

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b635badd5b6ee39889ef7146e5e839d0.png

CVP 类型:绿蓝色盲

事后看来,我应该选择不同的调色板。Adobe Color(Adobe Color)提供了构建对 CVP 用户可访问的调色板的出色工具:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/11989f5446967edba0959c2f7e34c7cf.png

Adobe Color 网站展示了不同类型的 CVP 用户看到的调色板是什么样的,并突出了潜在问题。

在低亮度界面上编辑

在过去,我通过艰难的方式学到了以下价值:

  1. 一台校准良好的显示器。

  2. 能够精确控制亮度(以保持一致性)。

不幸的是,我没有在 Illustrator 中查看具有较浅界面背景的最终图形。这会让我在导出之前发现图像有点暗。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e1da88c8481051400516c377bf936732.png

当与深色背景对比时,事物往往看起来更亮。


最终评论

  • 使用 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 仓库中找到(可能与这里展示的略有不同)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值