Timeline con matplotlib.
Tags: matplotlib, timeline, python
Published: 2021-10-03 13:54:48

Si estás aquí, lo más probable es que ya conozcas **matplotlib**. Y si es así, casi seguro que te habrá sorprendido la elección de esta librería para representar un **timeline**.

Lo cierto es que el procedimiento para obtener un resultado decente es algo engorroso. Pero resolver este tipo de circunstancias es una de las cosas que justifica nuestro trabajo como desarrolladores. Así que, ¿te interesa una función que genere un gráfico como el siguiente? 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/95go075xlv2qwgys01rb.png)

Bueno, ha llegado la hora de poner manos a la obra, así que vamos a crear un entorno virtual e instalar las librerías necesarias.

```bash
mkdir matplotlib_timeline
cd matplotlib_timeline
python3 -m venv venv
source venv/bin/activate
pip3 install matplotlib
```

Ahora, abrimos nuetro editor favorito y tecleamos el siguiente código:

```python
import matplotlib.pylab as plt

def timeline(titulo, eventos_fechas, eventos_textos, eventos_delta_y):

    # Representaremos los años dentro de una línea de 10 unidades
    ESCALA = 10.0

    # Dejaremos un margen e una unidad, por delante y por detrás en el eje X, por lo que el gráfico
    # contendrá 12 unidades (las 10 de los años + 2 de los márgenes).
    MARGEN = 1
    MARGEN_DELTA = 1.0 / (ESCALA + MARGEN * 2) 

    # Averiguamos la distancia en años entre el primer acontecimiento y el último, para calcular
    # a cuantas unidades de nuestro eje X se corresponde un año. 
    fecha_minimo = min(eventos_fechas)
    fecha_maximo = max(eventos_fechas)
    fecha_rango = fecha_maximo - fecha_minimo
    fecha_posicion = ESCALA / fecha_rango
    
    # Generamos una lista con la posición relativa que cada año tiene que ocupar dentro del eje X
    # Y por comodidad, una lista de posición en el eje Y, para los puntos y las marcas de texto
    fechas_x = [(fecha - fecha_minimo) * fecha_posicion + MARGEN for fecha in eventos_fechas]
    fechas_y = [0 for fecha in eventos_fechas]
    lineas_y = [1 for fecha in eventos_fechas]
    
    # Creamos el gŕafico y establecemos los límites que nos interesan
    fig, ax = plt.subplots(figsize=(15, 8),  constrained_layout=True)
    ax.set_ylim(-2, 2)
    ax.set_xlim(0, ESCALA + MARGEN * 2)
    ax.set_title(titulo, fontweight="bold", fontfamily='sans', fontsize=22, color='#4a4a4a')

    # Dibujamos la línea sobre la que representar los puntos del timeline
    ax.axhline(0, xmin=MARGEN_DELTA, xmax=1-MARGEN_DELTA, c='#4a4a4a', zorder=1)

    # Dibujamos sobre la línea los puntos correspondientes a cada año de la lista de acontecimientos.
    # Primero un círculo grande y después otro más pequeño encima, para dar la sensación de una
    # circunferencia con borde y rellena de color.
    ax.scatter(fechas_x, fechas_y, s=120, c='#4a4a4a', zorder=2)
    ax.scatter(fechas_x, fechas_y, s=30, c='#faff00', zorder=3)

    # Establecemos el tamaño de los texto, así como la separación vertical respecto a la
    # linea vertical de marca. 
    FONT_SIZE = 12
    DELTA_TEXTO = 0.1

    # Determinamos la posición que debe ocupara cada texto dentro del gráfico
    # En el eje X coincide con la posición del año sobre el eje.
    # En el eje Y lo determina el desplazamiento recibido como parámetro, más la separación
    # adicional estipulada. 
    for x, fecha, texto, delta_y  in zip(fechas_x, eventos_fechas, eventos_textos, eventos_delta_y ):
        
        # El cálculo es diferente para valores positivos y negativos
        DELTA_TEXTO_y = delta_y + DELTA_TEXTO if delta_y > 0 else delta_y - DELTA_TEXTO
        
        # Cambiamos la alineación vertical en función de si están por enciama, o por debajo
        # del eje. Así nos aseguramos una disposición consistente en el caso de textos con
        # más de una línea (sin necesidad de tener que ajustar la posición en función
        # del tamaño real del texto)
        va = 'bottom' if delta_y > 0 else 'top'
        
        # Para mayor claridad, anteponemos al texto un línea con el año al que corresponde  
        ax.text(
            x, DELTA_TEXTO_y, str(fecha) + "\n" + texto, 
            ha='center', va=va, 
            fontfamily='sans', fontweight='bold', fontsize=FONT_SIZE,
            color='#4a4a4a'
        )

    # Establecemos márcas verticales para cada acontecimiento. La posición en el eje X,
    # nuevamente, la determina el año, y la longitud vertical la que hemos recibido como
    # parámetro (con la que hemos ajustado la distancia vertical del texto)
    markerline, stemline, baseline = ax.stem(fechas_x, eventos_delta_y, use_line_collection=True)

    # Damos estilo a las marcas verticales    
    plt.setp(baseline, zorder=0)
    plt.setp(markerline, marker='', color='#4a4a4a')
    plt.setp(stemline, color='#4a4a4a')


    # Para que el gŕafico quede más limpio, eliminamos las marcas de los ejes ejes
    ax.set_xticks([]) 
    ax.set_yticks([]) 

    # También eliminamos líneas del marco
    for spine in ["left", "top", "right", "bottom"]:
        ax.spines[spine].set_visible(False)

    # Mostramos resultado    
    plt.show()


# Título del timeline
titulo = "\nNuevos antibióticos desde el año 2000"

# Relación de años a mostrar en el timeline
eventos_fechas = [
    2000,
    2001,
    2003,
    2005,
    2005,
    2009,
    2010,
    2011,
    2012,
    2013,
    2014,
    2014,
    2015,
    2017,
    2019,
    2019,
]   

# Evento ocurrido en el año de la relación anterior
eventos_textos = [
    "linezolid",
    "elithromycin",
    "daptomycin",
    "tigecycline",
    "doripenem",
    "telavancin",
    "ceftaroline",
    "fidaxomicin",
    "bedaquiline",
    "telavancin",
    "tedizolid",
    "dalbavancin\nceftolozane\ntazobactam",
    "ceftazidime\navibactam",
    "meropenem\nvaborbactam",
    "imipenem\ncilastatin\nrelebactam",
    "cefiderocol",
]

# Dado que dependiendo de la longitud de los texto, es bastante probable el solapamiento
# de los mismmos, para cada uno de ellos indicamos el desplazamiento respecto al eje central.
eventos_delta_y = [
    0.5,
   -0.5,
    0.5,
   -0.5,
    0.5,
   -0.5,
    0.5,
   -0.5,
    0.5,
   -0.5,
    0.5,
   -0.8,
    0.8,
   -0.5,
    0.5,
   -0.5,
]

# Llamamos a la función que genera el gráfico del timeline
timeline(titulo, eventos_fechas, eventos_textos, eventos_delta_y)
```

Como puedes comprobar, he añadido comentarios a casi todas las líneas, por lo que no creo que sea necesario hacer explicaciones adicionales al còdigo.

La función espera recibir el título, y **3 listas** con los datos a representar, después se encarga de hacer el resto del trabajo sucio.

* Lista de **años** para cada evento a representar.
* Lista con los **textos** descriptivos del evento.
* Lista con **desplazamiento vertical** de los textos, respecto al **eje central**. Jugando con estos valores, podemos lidiar con, el más que probable solapamiento de textos, entre eventos próximos en el tiempo.

Al final, creo que noa ha quedado una función que permite obtener un resultado ***decente***, con muy poco esfuerzo.

Espero que la disfrutes.