Crear gráficos narrativos de estilo xkcd

45

En una de las tiras xkcd más icónicas, Randall Munroe visualizó las líneas de tiempo de varias películas en gráficos narrativos:

ingrese la descripción de la imagen aquí (Haga clic para una versión más grande).

Fuente: xkcd No. 657 .

Dada una especificación de la línea de tiempo de una película (o alguna otra narrativa), debe generar dicho gráfico. Este es un concurso de popularidad, por lo que la respuesta con más votos (netos) ganará.

Requerimientos mínimos

Para ajustar un poco la especificación, aquí está el conjunto mínimo de características que cada respuesta debe implementar:

  • Tome como entrada una lista de nombres de personajes, seguida de una lista de eventos. Cada evento es una lista de personajes moribundos o una lista de grupos de personajes (lo que significa qué personajes están actualmente juntos). Aquí hay un ejemplo de cómo se podría codificar la narrativa de Jurassic Park:

    ["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]]
    ]
    

    Por ejemplo, la primera línea significa que al comienzo de la tabla, T-Rex está solo, los tres Raptors están juntos, Malcolm está solo, Grant y Sattler están juntos, etc. El penúltimo evento significa que dos de los Raptors mueren .

    De qué manera exactamente espera que la entrada dependa de usted, siempre que se pueda especificar este tipo de información. Por ejemplo, puede usar cualquier formato de lista conveniente. También puede esperar que los personajes de los eventos vuelvan a ser los nombres completos de los personajes, etc.

    Puede (pero no tiene que) suponer que cada lista de grupos contiene cada personaje vivo en exactamente un grupo. Sin embargo, debe no asumir que los grupos o personajes dentro de un evento están en orden particularmente conveniente.

  • Renderice a la pantalla o al archivo (como un gráfico vectorial o ráster) un gráfico que tenga una línea para cada carácter. Cada línea debe estar etiquetada con un nombre de carácter al comienzo de la línea.

  • Para cada evento normal, debe haber, en orden, una sección transversal de la tabla en la que los grupos de caracteres se parezcan claramente por la proximidad de sus líneas respectivas.
  • Para cada evento de muerte, las líneas de los caracteres relevantes deben terminar en un blob visible.
  • Usted no tiene que reproducir cualesquiera otras características de las parcelas de Randall, ni tiene que reproducir su estilo de dibujo. Líneas rectas con curvas cerradas, todo en negro, sin más etiquetas y un título está perfectamente bien para entrar en la competencia. Tampoco es necesario usar el espacio de manera eficiente; por ejemplo, podría simplificar su algoritmo moviendo solo líneas hacia abajo para encontrarse con otros personajes, siempre que haya una dirección de tiempo discernible.

He agregado una solución de referencia que cumple exactamente estos requisitos mínimos.

Haciéndolo bonito

Sin embargo, este es un concurso de popularidad, así que además de eso, puedes implementar cualquier fantasía que desees. La adición más importante es un algoritmo de diseño decente que hace que el gráfico sea más legible, por ejemplo, que hace que las curvas en las líneas sean fáciles de seguir y que reduce el número de cruces de línea necesarios. ¡Este es el problema algorítmico central de este desafío! Los votos decidirán qué tan bien funciona su algoritmo para mantener el gráfico ordenado.

Pero aquí hay algunas ideas más, la mayoría de ellas basadas en las listas de Randall:

Decoraciones:

  • Líneas de colores
  • Un título para la trama.
  • La línea de etiquetado termina.
  • Volver a etiquetar automáticamente las líneas que han pasado por una sección ocupada.
  • Estilo dibujado a mano (u otro? Como dije, no hay necesidad de reproducir el estilo de Randall si tiene una mejor idea) para líneas y fuentes.
  • Orientación personalizable del eje de tiempo.

Expresividad adicional:

  • Eventos nombrados / grupos / muertes.
  • Líneas que desaparecen y reaparecen.
  • Personajes entrando tarde.
  • Aspectos destacados que indican propiedades (¿transferibles?) De los caracteres (p. Ej., Vea el portador del anillo en el gráfico LotR).
  • Codificación de información adicional en el eje de agrupación (por ejemplo, información geográfica como en el gráfico LotR).
  • ¿Viaje en el tiempo?
  • Realidades alternativas?
  • ¿Un personaje que se convierte en otro?
  • ¿Dos personajes fusionándose? (¿Un personaje dividiéndose?)
  • 3D? (Si realmente llega tan lejos, ¡asegúrese de estar usando la dimensión adicional para visualizar algo!)
  • Cualquier otra característica relevante, que podría ser útil para visualizar la narrativa de una película (o libro, etc.).

Por supuesto, muchos de estos requerirán una entrada adicional, y puede aumentar su formato de entrada según sea necesario, pero documente cómo se pueden ingresar los datos.

Incluya uno o dos ejemplos para mostrar las características que implementó.

Su solución debería ser capaz de manejar cualquier entrada válida, pero está absolutamente bien si se adapta mejor a ciertos tipos de narrativas que a otras.

Criterios de votación

No me hago ilusiones de que podría decirle a la gente cómo deberían gastar sus votos, pero aquí hay algunas pautas sugeridas en orden de importancia:

  • Respuestas negativas que explotan las lagunas, las estándar u otras, o codifican uno o más resultados.
  • No eleve las respuestas que no cumplan con los requisitos mínimos (no importa cuán elegante sea el resto).
  • En primer lugar, votar por buenos algoritmos de diseño. Esto incluye respuestas que no usan mucho espacio vertical mientras minimizan el cruce de líneas para mantener legible el gráfico, o que logran codificar información adicional en el eje vertical. Visualizar las agrupaciones sin hacer un gran desastre debería ser el foco principal de este desafío, de modo que siga siendo un concurso de programación con un interesante problema algorítmico en el corazón.
  • Vota características opcionales que agregan poder expresivo (es decir, no son solo decoración pura).
  • Por último, upvote buena presentación.
Martin Ender
fuente
77
porque code-golf no tiene suficiente xkcd
orgulloso haskeller
8
@proudhaskeller PPCG nunca puede tener suficiente xkcd. ;) Pero no creo que hayamos intentado desafiar aún más sus gráficos / visualizaciones de información de gran tamaño, así que espero traer algo nuevo a la mesa con esto. Y estoy seguro de que algunos de los otros también harían desafíos muy diferentes e interesantes.
Martin Ender
¿Está bien si mi solución solo maneja a 12 hombres enojados, Duel (Spielberg, 1971, automovilista regular vs camionero loco) y Aviones, trenes y automóviles? ;-)
Level River St
44
Me pregunto cómo la entrada de imprimación se vería así ...
Joshua
1
@ping Sí, esa fue la idea. Si un evento contiene más listas, es una agrupación de listas. entonces [[x,y,z]]significaría que todos los personajes están actualmente juntos. Pero si el evento no contiene listas, sino solo personajes directamente, incluso es una muerte, por lo que en la misma situación [x,y,z]significa que esos tres personajes mueren. Siéntase libre de usar otro formato, con una indicación explícita de si algo es un evento de muerte o agrupación si eso lo ayuda. El formato anterior es solo una sugerencia. Siempre que su formato de entrada sea al menos tan expresivo, puede usar otra cosa.
Martin Ender

Respuestas:

18

Python3 con numpy, scipy y matplotlib

Parque jurásico

editar :

  • Traté de mantener los grupos en la misma posición relativa entre eventos, de ahí la sorted_eventfunción.
  • Nueva función para calcular la posición y de los caracteres ( coords).
  • Cada evento vivo se traza dos veces ahora, por lo que los personajes se unen mejor.
  • Se agregó la leyenda y se eliminó la etiqueta de los ejes.
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
fuente
Hah, muy bonito aspecto xkcd:) ... ¿hay alguna posibilidad de que puedas etiquetar las líneas?
Martin Ender
Rotule las líneas, tenga diferentes anchos de líneas (disminuyendo / aumentando entre algunos puntos) y finalmente ... haga las líneas más horizontales cuando estén cerca de un vértice mientras se interpolan, más como una curva bezier y esta sería la mejor entrada de la OMI: )
Optimizador
1
Gracias, pero el estilo xkcd está incluido en matplotlib, por lo que fue solo una llamada a la función :) Bueno, creé una leyenda, pero ocupaba casi un tercio de la imagen, así que lo comenté.
pgy
Modifiqué mi respuesta, creo que se ve mejor ahora.
pgy
6

T-SQL

No estoy contento con esto como una entrada, pero creo que esta pregunta merece al menos intentarlo. Intentaré mejorar este tiempo más tarde si lo permite, pero el etiquetado siempre será un problema en SQL. La solución requiere SQL 2012+ y se ejecuta en SSMS (SQL Server Management Studio). El resultado está en la pestaña de resultados espaciales.

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

La línea de tiempo resultante tiene el siguiente aspecto ingrese la descripción de la imagen aquí

MickyT
fuente
4

Mathematica, Solución de referencia

Como referencia, proporciono un script de Mathematica que cumple exactamente los requisitos mínimos, nada más y nada menos.

Espera que los caracteres sean una lista del formato en la pregunta charsy los eventos en 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
  }
 ]

Como ejemplo, aquí está el ejemplo de Jurassic Park usando el tipo de lista de 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}}
};

Nosotros recibiremos:

ingrese la descripción de la imagen aquí

(Haga clic para una versión más grande).

Eso no se ve tan mal, pero eso se debe principalmente a que los datos de entrada están más o menos ordenados. Si barajamos los grupos y los personajes en cada evento (manteniendo la misma estructura), pueden suceder cosas como esta:

ingrese la descripción de la imagen aquí

Lo cual es un poco desordenado.

Entonces, como dije, esto solo cumple los requisitos mínimos. No trata de encontrar un diseño agradable y no es bonito, ¡pero ahí es donde entran ustedes!

Martin Ender
fuente
¿Pensé que tal vez podrías 'embellecerlo' utilizando splines cuadráticas o cúbicas para eliminar las esquinas afiladas? (Lo haría de esa manera para que la tangente en los puntos dados sea siempre 0)
error
@flawr Claro, o podría aplicar algunos de estos trucos , pero ese no era el propósito de esta respuesta. ;) Realmente solo quería proporcionar una referencia para el mínimo absoluto.
Martin Ender
3
Oh, lo siento, ni siquiera me di cuenta de que esta era tu propia pregunta = P
falla