创建xkcd样式的叙述图


45

在更具标志性的xkcd片段之一中,Randall Munroe在叙述图表中可视化了几部电影的时间表:

在此处输入图片说明 (点击查看大图。)

资料来源:xkcd 657号

给定电影时间线的规格(或其他一些叙述),您将生成这样的图表。这是一次人气竞赛,因此(净)票数最多的答案将获胜。

最低要求

为了进一步规范,这是每个答案必须实现的最少功能集:

  • 输入一个字符名称列表,然后是一个事件列表。每个事件要么是垂死的字符列表,要么是字符组列表(表示当前在一起的字符)。这是侏罗纪公园叙述如何编码的一个示例:

    ["T-Rex", "Raptor", "Raptor", "Raptor", "Malcolm", "Grant", "Sattler", "Gennaro",
     "Hammond", "Kids", "Muldoon", "Arnold", "Nedry", "Dilophosaurus"]
    [
      [[0],[1,2,3],[4],[5,6],[7,8,10,11,12],[9],[13]],
      [[0],[1,2,3],[4,7,5,6,8,9,10,11,12],[13]],
      [[0],[1,2,3],[4,7,5,6,8,9,10],[11,12],[13]],
      [[0],[1,2,3],[4,7,5,6,9],[8,10,11,12],[13]],
      [[0,4,7],[1,2,3],[5,9],[6,8,10,11],[12],[13]],
      [7],
      [[5,9],[0],[4,6,10],[1,2,3],[8,11],[12,13]],
      [12],
      [[0, 5, 9], [1, 2, 3], [4, 6, 10, 8, 11], [13]], 
      [[0], [5, 9], [1, 2], [3, 11], [4, 6, 10, 8], [13]], 
      [11], 
      [[0], [5, 9], [1, 2, 10], [3, 6], [4, 8], [13]], 
      [10], 
      [[0], [1, 2, 9], [5, 6], [3], [4, 8], [13]], 
      [[0], [1], [9, 5, 6], [3], [4, 8], [2], [13]], 
      [[0, 1, 9, 5, 6, 3], [4, 8], [2], [13]], 
      [1, 3], 
      [[0], [9, 5, 6, 3, 4, 8], [2], [13]]
    ]
    

    例如,第一行表示在图表的开头,T-Rex是一个孤独者,三个猛龙队在一起,马尔科姆一个人,格兰特和萨特勒在一起,依此类推。倒数第二个事件意味着其中两个猛龙队死亡。

    只要可以指定此类信息,您将如何精确地期望输入取决于您。例如,您可以使用任何方便的列表格式。您还可以期望事件中的字符再次成为完整的字符名称,等等。

    您可以(但不必)假设每个组列表在一个组中都包含每个活动角色。但是,你应该不会假设组或角色一个事件是特别方便的顺序。

  • 渲染到屏幕或文件(作为矢量或光栅图形)的图表,每个字符有一行。每行必须在行的开头标记一个字符名称。

  • 对于每个正常事件,必须按顺序排列图表的某些横截面,在其中,各组字符通过其各自线条的接近度而清晰地相似。
  • 对于每个死亡事件,相关字符的行必须以可见的斑点结尾。
  • 不会有繁殖Randall的地块的其他任何功能,也不必重现他的绘画风格。带有急转弯的直线全部为黑色,没有进一步的标签,而且标题非常适合参加比赛。也不需要有效利用空间-例如,只要有明确的时间方向,就可以只向下移动行以与其他字符会合,从而有可能简化算法。

我添加了一个参考解决方案,它完全满足了这些最低要求。

使它漂亮

不过,这是一场人气竞赛,因此,最重要的是,您可以实现自己想要的任何幻想。最重要的增加是一种体面的布局算法,该算法使图表更清晰易读-例如,使线条的弯曲更易于跟踪,并减少了必要的线交叉数量。这是此挑战的核心算法问题!投票将决定您的算法在保持图表整洁方面的性能。

但是,这里还有其他一些想法,其中大多数是根据Randall的图表得出的:

装饰物:

  • 彩色的线条。
  • 地块的标题。
  • 标签线末端。
  • 自动重新标记经过繁忙部分的线路。
  • 线条和字体的手绘样式(或其他样式?如我所说,如果您有更好的主意,则无需复制Randall的样式)。
  • 时间轴的可自定义方向。

附加表现力:

  • 命名的事件/组/死亡。
  • 线条消失和重新出现。
  • 字符进入较晚。
  • 突出显示字符的属性(可转让?)(例如,请参阅LotR图表中的戒指持有者)。
  • 在分组轴上编码其他信息(例如,类似于LotR图表中的地理信息)。
  • 时间旅行?
  • 替代现实?
  • 一个角色变成另一个角色?
  • 两个字符合并?(字符拆分?)
  • 3D?(如果您确实走了那么远,请确保您实际上是在使用附加尺寸来可视化东西!)
  • 任何其他相关特征,可能对可视化电影(或书籍等)的叙事有用。

当然,其中许多将需要额外的输入,您可以根据需要随意扩展输入格式,但是请记录如何输入数据。

请提供一个或两个示例来展示您实现的功能。

您的解决方案应该能够处理任何有效的输入,但是如果它比其他叙事更适合某些叙事,那绝对没问题。

投票标准

我没有幻想可以告诉人们他们应该如何投票,但是按照重要性顺序,以下是一些建议的准则:

  • 使用漏洞,标准漏洞或其他漏洞或对一个或多个结果进行硬编码的否决答案。
  • 不要对未达到最低要求的答案进行投票(无论其余答案多么奇特)。
  • 首先,赞成好的布局算法。这包括不使用大量垂直空间,同时使线的交叉最小化以保持图形清晰的答案,或者设法将其他信息编码到垂直轴中的答案。可视化分组而不会造成很大的混乱,应该是这一挑战的主要重点,因此,这仍然是一场编程竞赛,其核心是一个有趣的算法问题。
  • 支持可增加表达力的可选功能(即不仅仅是纯粹的装饰)。
  • 最后,支持不错的演示文稿。

7
因为代码高尔夫没有足够的xkcd
骄傲的haskeller 2014年

8
@proudhaskeller PPCG永远不会有足够的xkcd。;)但是我认为我们还没有尝试过挑战他的超大信息图形/可视化,所以我希望我为此带来一些新的东西。而且我敢肯定,其他一些挑战也会带来非常不同和有趣的挑战。
马丁·恩德2014年

如果我的解决方案只处理12个生气的人,Duel(Spielberg,1971年,常规驾驶者与疯狂的卡车司机)以及飞机,火车和汽车,可以吗?;-)
Level River St

4
我不知道底漆的输入是什么样子的……
Joshua 2014年

1
@ping是的,就是这个想法。如果事件包含更多列表,则为列表分组。因此,[[x,y,z]]这意味着所有角色目前都在一起。但是,如果事件不包含列表,而仅包含字符,则甚至是死亡,因此在相同情况下[x,y,z],这三个字符将死亡。随意使用另一种格式,如果有帮助,它会明确指出某事物是死亡事件还是分组事件。以上格式仅是建议。只要您的输入格式至少具有表现力,就可以使用其他格式。
马丁·恩德2014年

Answers:


18

带有numpy,scipy和matplotlib的Python3

侏罗纪公园

编辑

  • 我试图使各组在事件之间保持相同的相对位置,因此在sorted_event功能上保持相同。
  • 用于计算字符y位置的新功能(coords)。
  • 现在,每个活动事件都会绘制两次,因此角色可以更好地粘在一起。
  • 添加了图例并删除了轴标签。
import math
import numpy as np
from scipy.interpolate import interp1d
from matplotlib import cm, pyplot as plt


def sorted_event(prev, event):
    """ Returns a new sorted event, where the order of the groups is
    similar to the order in the previous event. """
    similarity = lambda a, b: len(set(a) & set(b)) - len(set(a) ^ set(b))
    most_similar = lambda g: max(prev, key=lambda pg: similarity(g, pg))
    return sorted(event, key=lambda g: prev.index(most_similar(g)))


def parse_data(chars, events):
    """ Turns the input data into 3 "tables":
    - characters: {character_id: character_name}
    - timelines: {character_id: [y0, y1, y2, ...],
    - deaths: {character_id: (x, y)}
    where x and y are the coordinates of a point in the xkcd like plot.
    """
    characters = dict(enumerate(chars))
    deaths = {}
    timelines = {char: [] for char in characters}

    def coords(character, event):
        for gi, group in enumerate(event):
            if character in group:
                ci = group.index(character)
                return (gi + 0.5 * ci / len(group)) / len(event)
        return None

    t = 0
    previous = events[0]
    for event in events:
        if isinstance(event[0], list):
            previous = event = sorted_event(previous, event)
            for character in [c for c in characters if c not in deaths]:
                timelines[character] += [coords(character, event)] * 2
            t += 2
        else:
            for char in set(event) - set(deaths):
                deaths[char] = (t-1, timelines[char][-1])

    return characters, timelines, deaths


def plot_data(chars, timelines, deaths):
    """ Draws a nice xkcd like movie timeline """

    plt.xkcd()  # because python :)

    fig = plt.figure(figsize=(16,8))
    ax = fig.add_subplot(111)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    ax.set_xlim([0, max(map(len, timelines.values()))])

    color_floats = np.linspace(0, 1, len(chars))
    color_of = lambda char_id: cm.Accent(color_floats[char_id])

    for char_id in sorted(chars):
        y = timelines[char_id]
        f = interp1d(np.linspace(0, len(y)-1, len(y)), y, kind=5)
        x = np.linspace(0, len(y)-1, len(y)*10)
        ax.plot(x, f(x), c=color_of(char_id))

    x, y = zip(*(deaths[char_id] for char_id in sorted(deaths)))
    ax.scatter(x, y, c=np.array(list(map(color_of, sorted(deaths)))), 
               zorder=99, s=40)

    ax.legend(list(map(chars.get, sorted(chars))), loc='best', ncol=4)
    fig.savefig('testplot.png')


if __name__ == '__main__':
    chars = [
        "T-Rex","Raptor","Raptor","Raptor","Malcolm","Grant","Sattler",
        "Gennaro","Hammond","Kids","Muldoon","Arnold","Nedry","Dilophosaurus"
    ]
    events = [
        [[0],[1,2,3],[4],[5,6],[7,8,10,11,12],[9],[13]],
        [[0],[1,2,3],[4,7,5,6,8,9,10,11,12],[13]],
        [[0],[1,2,3],[4,7,5,6,8,9,10],[11,12],[13]],
        [[0],[1,2,3],[4,7,5,6,9],[8,10,11,12],[13]],
        [[0,4,7],[1,2,3],[5,9],[6,8,10,11],[12],[13]],
        [7],
        [[5,9],[0],[4,6,10],[1,2,3],[8,11],[12,13]],
        [12],
        [[0,5,9],[1,2,3],[4,6,10,8,11],[13]],
        [[0],[5,9],[1,2],[3,11],[4,6,10,8],[13]],
        [11],
        [[0],[5,9],[1,2,10],[3,6],[4,8],[13]],
        [10],
        [[0],[1,2,9],[5,6],[3],[4,8],[13]],
        [[0],[1],[9,5,6],[3],[4,8],[2],[13]],
        [[0,1,9,5,6,3],[4,8],[2],[13]],
        [1,3],
        [[0],[9,5,6,3,4,8],[2],[13]]
    ]
    plot_data(*parse_data(chars, events))

哈哈,非常漂亮的xkcd外观:)...有机会标记行吗?
马丁·恩德2014年

标记线条,使线条具有不同的宽度(在某些点之间减少/增加),最后...在插值时靠近顶点时使线条更水平,更像是贝塞尔曲线,这将是IMO的最佳输入: )
Optimizer

1
谢谢,但是xkcd样式包含在matplotlib中,因此它只是一个函数调用:)好吧,我创建了一个图例,但它占据了图像的近三分之一,因此我将其注释掉。
pgy 2014年

我修改了答案,我认为现在看起来更好。
pgy 2014年

6

T-SQL

我对此并不满意,但是我认为至少应该尝试一下这个问题。稍后,我将尝试改进此时间,但是在SQL中标记始终是一个问题。该解决方案需要SQL 2012+,并在SSMS(SQL Server Management Studio)中运行。输出在空间结果选项卡中。

-- Variables for the input
DECLARE @actors NVARCHAR(MAX) = '["T-Rex", "Raptor", "Raptor", "Raptor", "Malcolm", "Grant", "Sattler", "Gennaro", "Hammond", "Kids", "Muldoon", "Arnold", "Nedry", "Dilophosaurus"]';
DECLARE @timeline NVARCHAR(MAX) = '
[
   [[1], [2, 3, 4], [5], [6, 7], [8, 9, 11, 12, 13], [10], [14]],
   [[1], [2, 3, 4], [5, 8, 6, 7, 9, 10, 11, 12, 13], [14]],
   [[1], [2, 3, 4], [5, 8, 6, 7, 9, 10, 11], [12, 13], [14]],
   [[1], [2, 3, 4], [5, 8, 6, 7, 10], [9, 11, 12, 13], [14]],
   [[1, 5, 8], [2, 3, 4], [6, 10], [7, 9, 11, 12], [13], [14]],
   [8],
   [[6, 10], [1], [5, 7, 11], [2, 3, 4], [9, 12], [13, 14]],
   [13],
   [[1, 6, 10], [2, 3, 4], [5, 7, 11, 9, 12], [14]],
   [[1], [6, 10], [2, 3], [4, 12], [5, 7, 11, 9], [14]],
   [12],
   [[1], [6, 10], [2, 3, 11], [4, 7], [5, 9], [14]],
   [11],
   [[1], [2, 3, 10], [6, 7], [4], [5, 9], [14]],
   [[1], [2], [10, 6, 7], [4], [5, 9], [3], [14]],
   [[1, 2, 10, 6, 7, 4], [5, 9], [3], [14]],
   [2, 4],
   [[1], [10, 6, 7, 5, 9], [3], [14]]
]
';

-- Populate Actor table
WITH actor(A) AS ( SELECT CAST(REPLACE(STUFF(REPLACE(REPLACE(@actors,', ',','),'","','</a><a>'),1,2,'<a>'),'"]','</a>') AS XML))
SELECT ROW_NUMBER() OVER (ORDER BY(SELECT \)) ActorID, a.n.value('.','varchar(50)') Name
INTO Actor
FROM actor CROSS APPLY A.nodes('/a') as a(n);

-- Populate Timeline Table
WITH Seq(L) AS (
    SELECT CAST(REPLACE(REPLACE(REPLACE(REPLACE(@timeline,'[','<e>'),']','</e>'),'</e>,<e>','</e><e>'),'</e>,','</e>') AS XML)
    ),
    TimeLine(N,Exerpt,Elem) AS (
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) N
        ,z.query('.')
        ,CAST(REPLACE(CAST(z.query('.') AS VARCHAR(MAX)),',','</e><e>') AS XML)
    FROM Seq 
        CROSS APPLY Seq.L.nodes('/e/e') AS Z(Z)
    ),
    Groups(N,G,Exerpt) AS (
    SELECT N, 
        ROW_NUMBER() OVER (PARTITION BY N ORDER BY CAST(SUBSTRING(node.value('.','varchar(50)'),1,ISNULL(NULLIF(CHARINDEX(',',node.value('.','varchar(50)')),0),99)-1) AS INT)), 
        CAST(REPLACE(CAST(node.query('.') AS VARCHAR(MAX)),',','</e><e>') AS XML) C
    FROM TimeLine 
        CROSS APPLY Exerpt.nodes('/e/e') as Z(node)
    WHERE Exerpt.exist('/e/e') = 1
    )
SELECT * 
INTO TimeLine
FROM (
    SELECT N, null G, null P, node.value('.','int') ActorID, 1 D 
    FROM TimeLine CROSS APPLY TimeLine.Elem.nodes('/e') AS E(node)
    WHERE Exerpt.exist('/e/e') = 0
    UNION ALL
    SELECT N, G, DENSE_RANK() OVER (PARTITION BY N, G ORDER BY node.value('.','int')), node.value('.','int') ActorID, 0
    FROM Groups CROSS APPLY Groups.Exerpt.nodes('/e') AS D(node)
    ) z;

-- Sort the entries again
WITH ReOrder AS (
            SELECT *, 
                ROW_NUMBER() OVER (PARTITION BY N,G ORDER BY PG, ActorID) PP, 
                COUNT(P) OVER (PARTITION BY N,G) CP, 
                MAX(G) OVER (PARTITION BY N) MG, 
                MAX(ActorID) OVER (ORDER BY (SELECT\)) MA
            FROM (
                SELECT *,
                    LAG(G,1) OVER (PARTITION BY ActorID ORDER BY N) PG,
                    LEAD(G,1) OVER (PARTITION BY ActorID ORDER BY N) NG
                FROM timeline
                ) rg
    )
SELECT * INTO Reordered
FROM ReOrder;
ALTER TABLE Reordered ADD PPP INT
GO
ALTER TABLE Reordered ADD LPP INT
GO
WITH U AS (SELECT N, P, LPP, LAG(PP,1) OVER (PARTITION BY ActorID ORDER BY N) X FROM Reordered)
UPDATE U SET LPP = X FROM U;
WITH U AS (SELECT N, ActorID, P, PG, LPP, PPP, DENSE_RANK() OVER (PARTITION BY N,G ORDER BY PG, LPP) X FROM Reordered)
UPDATE U SET PPP = X FROM U;
GO

SELECT Name, 
    Geometry::STGeomFromText(
        STUFF(LS,1,2,'LINESTRING (') + ')'
        ,0)
        .STBuffer(.1)
        .STUnion(
        Geometry::STGeomFromText('POINT (' + REVERSE(SUBSTRING(REVERSE(LS),1,CHARINDEX(',',REVERSE(LS))-1)) + ')',0).STBuffer(D*.4)
        )
FROM Actor a
    CROSS APPLY (
        SELECT CONCAT(', '
            ,((N*5)-1.2)
                ,' ',(G)+P
            ,', '
            ,((N*5)+1.2)
                ,' ',(G)+P 
            ) AS [text()]
        FROM (
            SELECT ActorID, N,
                CASE WHEN d = 1 THEN
                    ((MA+.0) / (LAG(MG,1) OVER (PARTITION BY ActorID ORDER BY N)+.0)) * 
                    PG * 1.2
                ELSE 
                    ((MA+.0) / (MG+.0)) * 
                    G * 1.2
                END G,
                CASE WHEN d = 1 THEN
                (LAG(PPP,1) OVER (PARTITION BY ActorID ORDER BY N) -((LAG(CP,1) OVER (PARTITION BY ActorID ORDER BY N)-1)/2)) * .2 
                ELSE
                (PPP-((CP-1)/2)) * .2 
                END P
                ,PG
                ,NG
            FROM Reordered
            ) t
        WHERE a.actorid = t.actorid
        ORDER BY N, G
        FOR XML PATH('')
        ) x(LS)
    CROSS APPLY (SELECT MAX(D) d FROM TimeLine dt WHERE dt.ActorID = a.ActorID) d
GO

DROP TABLE Actor;
DROP TABLE Timeline;
DROP TABLE Reordered;

结果时间表如下所示 在此处输入图片说明


4

Mathematica,参考解决方案

作为参考,我提供了一个Mathematica脚本,该脚本完全满足最低要求,仅此而已。

它期望字符是问题中格式的列表chars,而事件中的事件是格式的列表events

n = Length@chars;
m = Max@Map[Length, events, {2}];
deaths = {};
Graphics[
 {
  PointSize@Large,
  (
     linePoints = If[Length@# == 3,
         lastPoint = {#[[1]], #[[2]] + #[[3]]/(m + 2)},
         AppendTo[deaths, Point@lastPoint]; lastPoint
         ] & /@ Position[events, #];
     {
      Line@linePoints,
      Text[chars[[#]], linePoints[[1]] - {.5, 0}]
      }
     ) & /@ Range@n,
  deaths
  }
 ]

例如,这是使用Mathematica的列表类型的侏罗纪公园示例:

chars = {"T-Rex", "Raptor", "Raptor", "Raptor", "Malcolm", "Grant", 
   "Sattler", "Gennaro", "Hammond", "Kids", "Muldoon", "Arnold", 
   "Nedry", "Dilophosaurus"};
events = {
   {{1}, {2, 3, 4}, {5}, {6, 7}, {8, 9, 11, 12, 13}, {10}, {14}},
   {{1}, {2, 3, 4}, {5, 8, 6, 7, 9, 10, 11, 12, 13}, {14}},
   {{1}, {2, 3, 4}, {5, 8, 6, 7, 9, 10, 11}, {12, 13}, {14}},
   {{1}, {2, 3, 4}, {5, 8, 6, 7, 10}, {9, 11, 12, 13}, {14}},
   {{1, 5, 8}, {2, 3, 4}, {6, 10}, {7, 9, 11, 12}, {13}, {14}},
   {8},
   {{6, 10}, {1}, {5, 7, 11}, {2, 3, 4}, {9, 12}, {13, 14}},
   {13},
   {{1, 6, 10}, {2, 3, 4}, {5, 7, 11, 9, 12}, {14}},
   {{1}, {6, 10}, {2, 3}, {4, 12}, {5, 7, 11, 9}, {14}},
   {12},
   {{1}, {6, 10}, {2, 3, 11}, {4, 7}, {5, 9}, {14}},
   {11},
   {{1}, {2, 3, 10}, {6, 7}, {4}, {5, 9}, {14}},
   {{1}, {2}, {10, 6, 7}, {4}, {5, 9}, {3}, {14}},
   {{1, 2, 10, 6, 7, 4}, {5, 9}, {3}, {14}},
   {2, 4},
   {{1}, {10, 6, 7, 4, 5, 9}, {3}, {14}}
};

我们会得到:

在此处输入图片说明

(点击查看大图。)

看起来不错,但这主要是因为输入数据或多或少是有序的。如果我们在每个事件中(在保持相同结构的同时)改组和字符,则可能会发生以下情况:

在此处输入图片说明

有点混乱。

因此,正如我所说,这仅满足最低要求。它不会尝试找到一个不错的布局,也不是​​很漂亮,但这就是你们来的地方!


我只是认为您可以通过使用二次或三次样条曲线“修饰”它来去除尖角?(我这样做是为了使给定点的切线始终为0)
虚假的

@flawr当然可以,或者我可以应用其中一些技巧,但这不是此答案的目的。;)我真的只是想为绝对最小值提供参考。
马丁·恩德

3
哦,对不起,您甚至没有注意到这是您自己的问题= P
虚假的2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.