Создание описательных диаграмм в стиле xkcd

45

В одной из самых знаковых полос 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 - одинокий, три Раптора - вместе, Малкольм - один, Грант и Саттлер - вместе, и т. Д. Со второго по последнее событие означает, что два из Хищников умирают ,

    Насколько точно вы ожидаете, что ввод зависит от вас, пока эта информация может быть указана. Например, вы можете использовать любой удобный формат списка. Вы также можете ожидать, что символы в событиях снова будут полными именами символов и т. Д.

    Вы можете (но не обязаны) предполагать, что каждый список групп содержит каждого живого персонажа ровно в одной группе. Однако, следует не считать , что группы или символы в пределах одного события в особенно удобном порядке.

  • Визуализируйте на экране или в файле (в виде векторной или растровой графики) диаграмму, которая имеет одну строку для каждого символа. Каждая строка должна быть помечена именем персонажа в начале строки.

  • Для каждого нормального события должен быть по порядку некоторый поперечный разрез диаграммы, в котором группы символов четко напоминают близость их соответствующих линий.
  • Для каждого события смерти строки соответствующих символов должны заканчиваться видимым большим двоичным объектом.
  • Вам не нужно воспроизводить какие-либо другие особенности сюжетов Рэндалла, а также не нужно воспроизводить его стиль рисования. Прямые линии с резкими поворотами, все в черном, без дальнейших надписей и заголовка, прекрасно подходят для участия в конкурсе. Также нет необходимости эффективно использовать пространство - например, вы могли бы потенциально упростить свой алгоритм, только перемещая линии вниз, чтобы встретиться с другими персонажами, если есть различимое направление времени.

Я добавил эталонное решение, которое точно соответствует этим минимальным требованиям.

Делая это довольно

Это конкурс популярности, так что, помимо этого, вы можете реализовать все, что захотите. Наиболее важным дополнением является приличный алгоритм компоновки, который делает диаграмму более разборчивой, например, которая позволяет легко отслеживать изгибы линий и уменьшает количество необходимых пересечений линий. Это основная алгоритмическая проблема этой задачи! Голоса решат, насколько хорошо ваш алгоритм работает в чистоте на графике.

Но вот еще несколько идей, большинство из которых основаны на диаграммах Рэндалла:

Украшения:

  • Цветные линии.
  • Название для сюжета.
  • Маркировка заканчивается.
  • Автоматически перемаркирующие линии, которые прошли через занятый участок.
  • Стиль, нарисованный от руки (или другой? Как я уже сказал, нет необходимости воспроизводить стиль Рэндалла, если у вас есть идея получше) для линий и шрифтов.
  • Настраиваемая ориентация оси времени.

Дополнительная выразительность:

  • Названные события / группы / смерти.
  • Исчезающие и вновь появляющиеся линии.
  • Персонажи входят поздно.
  • Выделены, которые указывают (переносимые?) Свойства символов (например, см. Несущий кольцо на диаграмме LotR).
  • Кодирование дополнительной информации по оси группировки (например, географическая информация, как на графике LotR).
  • Путешествие во времени?
  • Альтернативные реалии?
  • Персонаж превращается в другого?
  • Два персонажа сливаются? (Расщепление персонажа?)
  • 3D? (Если вы действительно зашли так далеко, пожалуйста, убедитесь, что вы действительно используете дополнительное измерение для визуализации чего-то!)
  • Любые другие важные функции, которые могут быть полезны для визуализации повествования фильма (или книги и т. Д.).

Конечно, многие из них потребуют дополнительного ввода, и вы можете при необходимости расширять свой формат ввода, но, пожалуйста, задокументируйте, как можно вводить данные.

Пожалуйста, включите один или два примера, чтобы продемонстрировать реализованные вами функции.

Ваше решение должно быть в состоянии справиться с любым действительным вкладом, но совершенно нормально, если оно лучше подходит для одних видов повествований, чем для других.

Критерии голосования

У меня нет иллюзий, что я могу рассказать людям, как они должны тратить свои голоса, но вот некоторые предлагаемые рекомендации в порядке важности:

  • Downvote ответы, которые используют лазейки, стандартные или другие, или жестко закодировать один или несколько результатов.
  • Не голосуйте за ответы, которые не отвечают минимальным требованиям (какими бы необычными они не были).
  • Прежде всего, поддержите хорошие алгоритмы компоновки. Это включает ответы, которые не используют много вертикального пространства при минимизации пересечения линий, чтобы сохранить четкость графика, или которым удается закодировать дополнительную информацию по вертикальной оси. Визуализация группировок без большого беспорядка должна быть главной целью этой задачи, так что это остается соревнованием по программированию с интересной алгоритмической проблемой в глубине души.
  • Upvote дополнительные функции, которые добавляют выразительную силу (то есть не просто украшение).
  • Наконец, upvote хорошая презентация.
Мартин Эндер
источник
7
потому что код-гольфу не хватает xkcd
гордый haskeller
8
@proudhaskeller PPCG никогда не может иметь достаточно xkcd. ;) Но я не думаю, что мы уже пытались оспаривать его расширенную инфо-графику / визуализации, поэтому я надеюсь, что с этим я привнесу что-то новое. И я уверен, что некоторые другие тоже будут делать очень разные и интересные задачи.
Мартин Эндер
Это нормально, если мое решение касается только 12 злых людей, Дуэль (Спилберг, 1971, обычный автомобилист против безумного дальнобойщика) и Самолеты, Поезда и Автомобили? ;-)
Level River St
4
Интересно, как будет выглядеть ввод для начинающих ...
Джошуа
1
@ping Да, это была идея. Если событие содержит дополнительные списки, это группировка списков. так [[x,y,z]]означало бы , что все символы в настоящее время вместе. Но если событие не содержит списков, а только символы напрямую, это даже смерть, поэтому в той же ситуации [x,y,z]эти три символа умирают. Не стесняйтесь использовать другой формат, с явным указанием того, является ли что-то смертью или групповым событием, если это вам помогает. Приведенный выше формат является лишь предложением. Пока ваш формат ввода по крайней мере столь же выразителен, вы можете использовать что-то еще.
Мартин Эндер

Ответы:

18

Python3 с numpy, scipy и matplotlib

парк Юрского периода

редактировать :

  • Я пытался удерживать группы в одинаковом относительном положении между событиями, отсюда и 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))
PGY
источник
Ха, очень хороший вид xkcd:) ... есть ли шанс, что вы сможете пометить строки?
Мартин Эндер
Пометьте линии, разную ширину линий (с уменьшением / увеличением между некоторыми точками) и, наконец, ... сделайте линии более горизонтальными, когда они близки к вершине при интерполяции, больше похоже на кривую Безье, и это будет лучшим входом IMO: )
Оптимизатор
1
Спасибо, но стиль xkcd включен в matplotlib, так что это был только вызов функции :) Ну, я создал легенду, но она занимала почти треть изображения, поэтому я закомментировал ее.
pgy
Я изменил свой ответ, думаю, теперь он выглядит лучше.
pgy
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;

Итоговая временная шкала выглядит следующим образом введите описание изображения здесь

MickyT
источник
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
@flawr Конечно, или я мог бы применить некоторые из этих уловок , но это не было целью этого ответа. ;) Я действительно просто хотел предоставить ссылку на абсолютный минимум.
Мартин Эндер
3
Извините, даже не заметил, что это был ваш собственный вопрос = P
flawr