Створення наративних діаграм у стилі xkcd


45

В одній з більш знакових смуг xkcd Рендалл Манро візуалізував часові рамки декількох фільмів у розповідних діаграмах:

введіть тут опис зображення (Клацніть для збільшення версії.)

Джерело: 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]]
    ]
    

    Наприклад, перший рядок означає, що на початку діаграми Т-Рекс - самотній, три Раптори разом, Малькольм один, Грант і Саттлер разом і т.д. .

    Як саме ви очікуєте, що від вас залежить, якщо ви зможете вказати подібну інформацію. Наприклад, ви можете використовувати будь-який зручний формат списку. Ви також можете розраховувати, що герої в подіях знову стануть повними іменами символів тощо.

    Ви можете (але не обов’язково) припускати, що кожен список груп містить кожного живого символу саме в одній групі. Однак, слід НЕ вважати , що групи або символи в межах однієї події в особливо зручному порядку.

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

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

Я додав довідкове рішення, яке відповідає саме цим мінімальним вимогам.

Зробити це досить

Це змагання за популярність, окрім цього ви можете реалізовувати будь-яку фантазію. Найважливішим доповненням є пристойний алгоритм компонування, який робить діаграму більш розбірливою - наприклад, що робить вигини в лініях легко слідувати і що зменшує кількість необхідних перетинів ліній. Це основна алгоритмічна проблема цього завдання! Голоси визначають, наскільки добре працює ваш алгоритм, підтримуючи чіткий графік.

Але ось ще кілька ідей, більшість з яких ґрунтуються на графіках Рендала:

Прикраси:

  • Кольорові лінії.
  • Назва сюжету.
  • Лінія маркування закінчується.
  • Автоматичне відновлення ліній, які пройшли через зайнятий ділянку.
  • Стиль, намальований вручну (або інший? Як я вже сказав, не потрібно відтворювати стиль Рандала, якщо у вас є краща ідея) для ліній та шрифтів.
  • Настроювана орієнтація осі часу.

Додаткова виразність:

  • Названі події / групи / смерті.
  • Зникаючі та знову з'являються лінії.
  • Персонажі, що заходять пізно.
  • Основні моменти, які вказують (передаються?) Властивості символів (наприклад, дивіться кільцеву носію в діаграмі LotR).
  • Кодування додаткової інформації по осі групування (наприклад, географічна інформація, як на графіку LotR).
  • Час у дорозі?
  • Альтернативні реалії?
  • Персонаж перетворюється на іншого?
  • Два символи, що зливаються? (Розбиття персонажа?)
  • 3D? (Якщо ви дійсно так далеко, переконайтеся, що ви використовуєте додатковий вимір для візуалізації чогось!)
  • Будь-які інші відповідні функції, які могли б бути корисними для візуалізації розповіді про фільм (або книгу тощо).

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

Додайте один-два приклади, щоб показати функції, які ви впровадили.

У вашому рішенні має бути можливість вирішувати будь-які дійсні дані, але це абсолютно добре, якщо воно краще підходить до певних видів розповідей, ніж інші.

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

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

  • Відповіді Downvote, які використовують лазівки, стандартні ті чи інші, або жорсткий код одного або декількох результатів.
  • Не заявляйте відповіді, які не відповідають мінімальним вимогам (якими б фантазійними не були інші).
  • Перш за все, запропонуйте приємні алгоритми компонування. Сюди входять відповіді, які не використовують багато вертикального простору, зводячи до мінімуму перетинання ліній, щоб зберегти графік розбірливим, або яким вдалося кодувати додаткову інформацію у вертикальну вісь. Візуалізація групувань, не створюючи великого безладу, повинна бути головним напрямком цього виклику, таким чином, щоб це залишалось конкурсом програмування з цікавою алгоритмічною проблемою.
  • Оновіть додаткові функції, які додають виразної сили (тобто не є лише чистою прикрасою).
  • Нарешті, підкреслити приємну презентацію.

7
тому що у коду-гольфу недостатньо xkcd
гордий haskeller

8
@proudhaskeller PPCG ніколи не може мати достатньо xkcd. ;) Але я не думаю, що ми ще намагалися скласти виклики його надмірній графіці / візуалізації інформації, тому сподіваюся, що я приношу щось нове з цим. І я впевнений, що деякі з інших також склали б дуже різні та цікаві виклики.
Мартін Ендер

Чи нормально, якщо в моєму рішенні працюють лише 12 розлючених чоловіків, поєдинок (Спілберг, 1971, штатний автомобіліст проти божевільного вантажника) та літаки, поїзди та автомобілі? ;-)
Рівень Рівер Сент

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

Так, дуже приємно виглядати 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;

Отримана часова шкала виглядає наступним чином введіть тут опис зображення


4

Математика, Довідкове рішення

Для довідки я надаю скрипт 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
недолік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.