xkcdスタイルのナラティブチャートを作成する


45

Randall Munroeは、より象徴的なxkcdストリップの1つで、いくつかの映画のタイムラインを物語チャートで視覚化しました。

ここに画像の説明を入力してください (クリックすると拡大します。)

出典: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は孤独、3つのRaptorsは一緒、Malcolmは単独、GrantとSattlerは一緒などを意味します。最後から2番目のイベントは、2つのRaptors 。

    この種の情報を指定できる限り、入力をどの程度正確に期待するかはあなた次第です。たとえば、便利なリスト形式を使用できます。また、イベント内のキャラクターが再び完全なキャラクター名になることも期待できます。

    グループの各リストには、1つのグループ内の各生きているキャラクターが含まれていると想定できます(ただし、そうする必要はありません)。ただし、1つのイベントのグループまたは文字が特に便利な順序であると想定しないでください。

  • 画面またはファイル(ベクトルまたはラスターグラフィック)に、各文字に1行のグラフを表示します。各行には、行の先頭に文字名のラベルを付ける必要があります。

  • 通常のイベントごとに、文字のグループがそれぞれの線の近接によって明確に似ているチャートの断面が順番に存在する必要があります。
  • 死亡イベントごとに、関連するキャラクターの行は目に見えるブロブで終了する必要があります。
  • あなたはないではないランドールのプロットの他の特徴を再現する必要があり、また、あなたは彼の描画スタイルを再現する必要があります。シャープな曲がりくねった直線、すべて黒で、追加のラベルとタイトルなしで、競争に参加するのにまったく問題ありません。また、スペースを効率的に使用する必要もありません。たとえば、識別可能な時間の方向がある限り、行を下に移動して他のキャラクターと会うだけで、アルゴリズムを単純化できる可能性があります。

これらの最小要件を正確に満たす参照ソリューションを追加しました。

きれいにする

ただし、これは人気のコンテストなので、その上で、好きなだけの空想を実装できます。最も重要な追加は、適切なレイアウトアルゴリズムです。これにより、グラフが読みやすくなります。たとえば、線の曲がりを追跡しやすくし、必要な線の交差の数を減らします。これがこの課題の中心的なアルゴリズムの問​​題です!票により、チャートの整頓を維持するアルゴリズムのパフォーマンスが決まります。

しかし、ここにいくつかのアイデアがあります。それらのほとんどは、ランドールのチャートに基づいています。

デコレーション:

  • 色付きの線。
  • プロットのタイトル。
  • ラベリング行が終了します。
  • 忙しいセクションを通過した行に自動的にラベルを付け直します。
  • 線とフォントの手書きスタイル(または他の?私が言ったように、より良いアイデアがあればランドールのスタイルを再現する必要はありません)。
  • 時間軸のカスタマイズ可能な方向。

追加の表現力:

  • 名前付きイベント/グループ/死。
  • 行の消失と再出現。
  • 遅く入るキャラクター。
  • キャラクターの(譲渡可能?)プロパティを示すハイライト(たとえば、LotRチャートのリングベアラーを参照)。
  • グループ化軸の追加情報のエンコード(LotRチャートのような地理情報など)。
  • タイムトラベル?
  • 代替現実?
  • 別のキャラクターに変わる?
  • 2つの文字がマージされますか?(文字分割?)
  • 3D?(本当にそこまで行けば、実際に何かを視覚化するために追加の次元を使用していることを確認してください!)
  • 映画(または本など)の物語を視覚化するのに役立つその他の関連機能。

もちろん、これらの多くは追加の入力を必要とし、必要に応じて入力形式を自由に拡張できますが、データの入力方法を文書化してください。

実装した機能を示すために、1つまたは2つの例を含めてください。

ソリューションは有効な入力を処理できる必要がありますが、特定の種類の物語に他の物語よりも適している場合は絶対に問題ありません。

投票基準

私は、人々がどのように投票を使うべきかを人々に伝えることができるという幻想は持っていませんが、重要な順に提案されたガイドラインがいくつかあります:

  • 抜け穴、標準的なもの、または他のものを悪用する、または1つ以上の結果をハードコードするダウンボートの回答。
  • 最低限の要件を満たしていない回答に賛成票を投じないでください(他の人がどんなに空想的であっても)。
  • 何よりもまず、素晴らしいレイアウトアルゴリズムを支持します。これには、グラフを読みやすくするために線の交差を最小限に抑えながら多くの垂直スペースを使用しない回答や、追加情報を垂直軸にエンコードする回答が含まれます。巨大な混乱を起こさずにグループ化を視覚化することがこの挑戦の主な焦点であるはずです。そのため、これは興味深いアルゴリズムの問​​題を中心にしたプログラミングコンテストのままです。
  • 表現力を追加するオプション機能を追加します(つまり、単なる装飾ではありません)。
  • 最後に、素敵なプレゼンテーションに賛成です。

7
code-golfに十分なxkcdがないため
誇りに思ってhaskeller 14年

8
@proudhaskeller PPCGは十分なxkcdを持つことはできません。;)しかし、私たちは彼の超大型の情報グラフィック/視覚化にまだ挑戦しようとはしていないと思うので、これでテーブルに新しいものをもたらすことを願っています。そして、他のいくつかは非常に異なった興味深い挑戦をするだろうと確信しています。
マーティンエンダー14年

私のソリューションが12人の怒っている男性、デュエル(スピルバーグ、1971年、通常の運転手対狂気のトラック運転手)、および飛行機、電車、自動車のみを処理するのであれば問題ありませんか?;-)
Level River St

4
私は...プライマーのための入力がどのように見えるだろうか疑問に思う
ジョシュア

1
@pingはい、それがアイデアでした。イベントにさらにリストが含まれる場合、それはリストのグループ化です。そう[[x,y,z]]、すべての文字が一緒に現在あることを意味します。ただし、イベントにリストが含まれておらず、キャラクターのみが直接含まれている場合は、死に至ることもあります。したがって、同じ状況で[x,y,z]は、これら3人のキャラクターが死亡します。別の形式を自由に使用してください。何かが死亡またはグループ化イベントであるかどうかが明示的に示されている場合は、それが役立ちます。上記の形式は単なる提案です。入力形式が少なくとも表現力豊かであれば、他のものを使用できます。
マーティンエンダー14年

回答:


18

numpy、scipy、matplotlibを備えたPython3

ジュラシック・パーク

編集

  • 私はグループをイベント間の同じ相対的な位置、つまりsorted_event機能に維持しようとしました。
  • 文字のy位置を計算する新しい関数(coords)。
  • 生きているイベントはすべて2回プロットされるようになったため、キャラクター同士の結びつきが良くなりました。
  • 凡例を追加し、軸ラベルを削除しました。
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の外観:)...行にラベルを付けることができますか?
マーティンエンダー14

ラインにラベルを付け、ラインの幅を変えて(いくつかのポイント間で減少/増加して)、最後に...補間しながら頂点に近づくとラインをより水平にし、ベジェ曲線のように、これが最良のエントリIMOです: )
オプティマイザー14

1
ありがとう、しかし、xkcdスタイルはmatplotlibに含まれているので、関数呼び出しのみでした:)さて、私は凡例を作成しましたが、画像のほぼ3分の1を占めていたのでコメントしました。
pgy 14

私は自分の答えを修正しました。今では良くなっていると思います。
14

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}}
};

取得します:

ここに画像の説明を入力してください

(クリックすると拡大します。)

見た目悪くありませんが、それは主に入力データが多かれ少なかれ順序付けられているためです。(同じ構造を維持しながら)各イベントでグループとキャラクターをシャッフルすると、次のようなことが起こります:

ここに画像の説明を入力してください

これは少し面倒です。

それで、私が言ったように、これは最小要件のみを満たします。素敵なレイアウトを見つけようとせず、見栄えもよくありませんが、そこがあなたの出番です!


おそらく、2次スプラインまたは3次スプラインを使用して鋭い角を削除することで、「プリティ」できると思いましたか?(私は与えられた点で接線が常に0であることをそのようにそれを行うだろう)
flawr

@flawr確かに、または私はこれらのトリックのいくつかを適用することができましたが、それはこの答えの目的ではありませんでした。;)絶対最小値の参照を提供したかっただけです。
マーティンエンダー14年

3
ああ申し訳ありませんが、これがあなた自身の質問であることに気付かなかった= P
flawr 14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.