Introducción

Este documento sistematiza, en formato de línea de tiempo interactiva, los principales hitos en materia de política educativa identificados a lo largo del semestre en los dos ámbitos de análisis del Observatorio: Poder Legislativo y Poder Ejecutivo. El ámbito de prensa, presente en versiones anteriores de este gráfico, fue eliminado por decisión del equipo al resultar redundante con los hitos ya registrados en los otros dos ámbitos.

La tabla de datos que alimenta el gráfico está pensada para actualizarse de manera continua, sesión a sesión, de modo que este .Rmd pueda reutilizarse en cada informe sin rehacer el trabajo de sistematización.

Estructura de datos

Cada fila representa un hito, con los siguientes campos:

  • evento: título corto del hito (aparece en el gráfico)
  • ambito: Legislativo / Ejecutivo
  • fecha_inicio / fecha_fin: si son iguales, el hito se grafica como punto; si son distintas, como rango (útil para tramitación de proyectos de ley)
  • detalle: descripción extendida, visible al pasar el mouse (hover)

Nota metodológica: en la medida en que esta tabla crezca, conviene externalizarla a un .csv (ej. hitos_educacion.csv) y leerla con read_csv() en vez de mantenerla como tribble() dentro del .Rmd. Esto facilita que distintos integrantes del equipo la actualicen sin tocar el documento.

Criterio de clasificación Ejecutivo / Legislativo

El criterio para asignar el ambito de un hito no es “quién lo origina”, sino dónde produce su efecto institucional:

Acto Ámbito Por qué
Anuncio o conferencia de prensa Depende de quién anuncia (Ejecutivo si es el presidente/ministro; Legislativo si es una bancada o comisión) Es un acto político real, se le asigna el ámbito de su emisor
Ingreso de un proyecto de ley (mensaje o moción) Legislativo Activa la tramitación parlamentaria, independiente de quién lo origine
Indicación (del Ejecutivo o de parlamentarios) a un proyecto en trámite Legislativo Solo produce efecto si es aprobada dentro del proceso legislativo
Sesión de comisión / votación Legislativo Ocurre dentro del Congreso
Decreto, resolución exenta, oficio circular Ejecutivo Tiene efecto inmediato, sin necesidad de aprobación parlamentaria
Promulgación Ejecutivo Acto final del Presidente, una vez completado el trámite en el Congreso

Por esto, cuando un mismo proceso combina un anuncio del Ejecutivo con su posterior tramitación legislativa (como la reforma al SAE), se registran como dos hitos separados: un punto Ejecutivo para el anuncio, y una estela Legislativa para el ingreso, las indicaciones y la votación.

# tipo -> define el símbolo del marcador (ver sección de diseño más abajo)
# relevancia -> ya no afecta el tamaño del marcador (ver nota en {r paleta}); se conserva
#               en el csv solo como referencia histórica, sin uso visual actual

hitos <- read_csv("hitos_educacion.csv", show_col_types = FALSE) %>%
  mutate(
    fecha_inicio = as.Date(fecha_inicio),
    fecha_fin    = as.Date(fecha_fin),
    es_rango     = fecha_inicio != fecha_fin,
    ambito       = factor(ambito, levels = c("Ejecutivo", "Legislativo"))
  )

hitos
## # A tibble: 15 × 9
##    evento ambito tipo  relevancia fecha_inicio fecha_fin  detalle url   es_rango
##    <chr>  <fct>  <chr>      <dbl> <date>       <date>     <chr>   <chr> <lgl>   
##  1 Ingre… Legis… Proy…          3 2026-04-07   2026-06-12 Proyec… <NA>  TRUE    
##  2 Aprob… Legis… Proy…          2 2026-05-05   2026-05-05 Aproba… <NA>  FALSE   
##  3 Sesió… Legis… Sesi…          2 2026-05-12   2026-05-12 Votaci… <NA>  FALSE   
##  4 Sesió… Legis… Sesi…          3 2026-06-02   2026-06-02 Aproba… <NA>  FALSE   
##  5 Sesió… Legis… Sesi…          3 2026-06-09   2026-06-09 Denunc… <NA>  FALSE   
##  6 Ofici… Ejecu… Medi…          3 2026-04-21   2026-04-21 Hacien… <NA>  FALSE   
##  7 Promu… Legis… Proy…          3 2026-03-25   2026-03-25 Marco … http… FALSE   
##  8 Resol… Ejecu… Medi…          3 2026-04-27   2026-04-27 Gobier… <NA>  FALSE   
##  9 TGR i… Ejecu… Medi…          1 2026-04-23   2026-04-23 Primer… <NA>  FALSE   
## 10 TGR a… Ejecu… Medi…          3 2026-06-04   2026-06-04 Segund… <NA>  FALSE   
## 11 Comis… Legis… Sesi…          1 2026-04-15   2026-04-15 El min… http… FALSE   
## 12 Kast … Legis… Proy…          3 2026-06-15   2026-06-15 Boletí… http… FALSE   
## 13 Comis… Legis… Sesi…          2 2026-07-08   2026-07-08 La Com… http… FALSE   
## 14 Anunc… Ejecu… Medi…          2 2026-06-23   2026-06-23 Kast a… http… FALSE   
## 15 Trami… Legis… Proy…          3 2026-06-23   2026-07-23 Ingres… http… TRUE

Línea de tiempo interactiva

Lógica de diseño

Para que el gráfico funcione como una pieza editorial y no como un Gantt genérico, se codifican tres dimensiones a la vez, distribuidas en dos filas (una por ámbito):

dimensión codifica valores
color del punto ámbito Legislativo (azul petróleo #2C5F6F) · Ejecutivo (burdeos #8B3A3A)
símbolo tipo de hito Proyecto de Ley (◆) · Sesión Comisión (●) · Medida Ejecutiva (▲)
tamaño uniforme para todos los hitos 13px fijos — se eliminó la variación por relevancia política por ser un criterio demasiado subjetivo
color de la estela evento específico (no el ámbito) cada proceso con rango de fechas puede tener su propio color en colores_rango, para distinguir visualmente “momentos políticos” distintos aunque compartan el mismo ámbito (ej. Escuelas Protegidas vs. reforma al SAE, ambos Legislativo)

Nota técnica: color, símbolo y tamaño se calculan como valores literales por fila en el chunk {r paleta} (columnas color_hex, symbol_val, size_px), y el gráfico se arma en tres llamadas a add_markers() separadas (una por tipo de hito), en vez de pasarle a plotly los datos categóricos crudos (color=~ambito, symbol=~tipo) para que los traduzca él mismo. Esto es deliberado: combinar dos argumentos categóricos en una misma llamada puede hacer que plotly ignore las paletas nombradas y aplique su propio orden alfabético por defecto.

Además se agregan líneas verticales punteadas para marcar el cambio de mes y un fondo cálido tipo papel, para alejarse de la grilla blanca por defecto de plotly.

colores_ambito <- c(
  "Legislativo" = "#2C5F6F",
  "Ejecutivo"   = "#8B3A3A"
)

simbolos_tipo <- c(
  "Proyecto de Ley"  = "diamond",
  "Sesión Comisión"  = "circle",
  "Medida Ejecutiva" = "triangle-up"
)

# IMPORTANTE — por qué esto se calcula acá y no se deja como argumento de plotly:
# pasarle a add_markers() dos argumentos categóricos a la vez (color=~ambito y
# symbol=~tipo) hace que plotly, en ciertas versiones, ignore las paletas nombradas
# que le pasamos y aplique su propio orden alfabético por defecto para alguna de
# las dos dimensiones (por eso podían aparecer símbolos o colores que no correspondían
# a lo definido arriba). La forma a prueba de fallos es calcular acá, fila por fila,
# el color, el símbolo y el tamaño en píxeles ya resueltos como valores literales, y
# más abajo separar los datos en tres grupos reales (uno por tipo de hito) para que
# cada uno se dibuje con su propio color/símbolo fijo, sin que plotly tenga que inferir nada.
hitos <- hitos %>%
  mutate(
    color_hex = case_when(
      ambito == "Ejecutivo"   ~ colores_ambito[["Ejecutivo"]],
      ambito == "Legislativo" ~ colores_ambito[["Legislativo"]],
      TRUE ~ "#999999"
    ),
    symbol_val = case_when(
      tipo == "Proyecto de Ley"  ~ simbolos_tipo[["Proyecto de Ley"]],
      tipo == "Sesión Comisión"  ~ simbolos_tipo[["Sesión Comisión"]],
      tipo == "Medida Ejecutiva" ~ simbolos_tipo[["Medida Ejecutiva"]],
      TRUE ~ "circle"
    ),
    # Tamaño uniforme para todos los marcadores: se eliminó la variación por
    # "relevancia" porque asignarle un valor de 1 a 3 a cada hito resultaba
    # demasiado subjetivo entre distintas personas del equipo. La columna
    # `relevancia` puede seguir existiendo en el csv (por si se retoma más
    # adelante), pero ya no afecta el tamaño del punto.
    size_px = 13,
    # posición numérica en el eje y (en vez de dejar que plotly reparta las dos
    # categorías con su espaciado fijo por defecto): así se puede acercar o alejar
    # "Ejecutivo" y "Legislativo" ajustando solo estos dos números.
    y_pos = case_when(
      ambito == "Ejecutivo"   ~ 1,
      ambito == "Legislativo" ~ 1.35,
      TRUE ~ 1.2
    )
  )

# Color de las estelas (barras de rango): por defecto sigue el color del ámbito,
# pero se puede fijar un color propio por evento cuando dos procesos del mismo
# ámbito conviene distinguir visualmente como "momentos políticos" distintos.
colores_rango <- c(
  "Ingreso 'Escuelas Protegidas' (18156-04)"     = "#2C5F6F",
  "Tramitación reforma al SAE (Elección Mutua)"  = "#B08D57"
)

color_estela <- function(evento, ambito) {
  if (evento %in% names(colores_rango)) {
    colores_rango[[evento]]
  } else {
    colores_ambito[[as.character(ambito)]]
  }
}

# Meses en español, mapeados a mano para no depender del locale del sistema
# (format(fecha, "%B") de R queda en inglés si el locale de la máquina no está en español,
# lo cual varía entre Windows/Mac/Linux y entre computadores del equipo).
meses_es <- c("enero", "febrero", "marzo", "abril", "mayo", "junio",
              "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre")

fecha_es <- function(fecha) {
  paste0(format(fecha, "%d"), " de ", meses_es[as.integer(format(fecha, "%m"))],
         " de ", format(fecha, "%Y"))
}

# Envuelve textos largos del hover en varias líneas (en vez de una sola línea
# gigante), insertando <br> en los cortes de palabra. Sin esto, un `detalle`
# extenso hace que el tooltip se estire más allá del ancho de la pantalla,
# incluso en un monitor grande.
wrap_hover <- function(texto, ancho = 52) {
  vapply(texto, function(t) paste(strwrap(t, width = ancho), collapse = "<br>"),
         character(1), USE.NAMES = FALSE)
}
fig <- plot_ly()

# Barras translúcidas para procesos con rango de fechas (tramitaciones)
rangos <- hitos %>% filter(es_rango)

for (i in seq_len(nrow(rangos))) {
  # add_segments() solo genera 2 puntos de datos (inicio y fin), y plotly únicamente
  # activa el hover exactamente sobre un punto de datos real, no en cualquier lugar
  # del trazo. Por eso se generan puntos intermedios cada 2 días a lo largo de todo
  # el rango, todos con el mismo texto: así el hover funciona en toda la banda,
  # no solo en sus extremos.
  fechas_rango <- seq(rangos$fecha_inicio[i], rangos$fecha_fin[i], by = "2 days")
  texto_rango <- paste0(
    "<b>", rangos$evento[i], "</b><br>",
    fecha_es(rangos$fecha_inicio[i]), " – ", fecha_es(rangos$fecha_fin[i]),
    "<br><br>", wrap_hover(rangos$detalle[i])
  )

  fig <- fig %>% add_trace(
    type = "scatter", mode = "lines",
    x = fechas_rango, y = rep(rangos$y_pos[i], length(fechas_rango)),
    line = list(color = color_estela(rangos$evento[i], rangos$ambito[i]), width = 18),
    opacity = 0.22,
    showlegend = FALSE,
    text = rep(texto_rango, length(fechas_rango)),
    hoverinfo = "text",
    hoverlabel = list(bgcolor = "#FAF7F0", font = list(family = "Georgia", size = 12, color = "#3B3B3B"),
                       bordercolor = "#D8D2C4")
  )
}

# Separadores de mes (líneas verticales punteadas + etiqueta) 
meses <- seq(as.Date("2026-03-01"), as.Date("2026-08-01"), by = "month")

shapes_mes <- lapply(meses, function(m) {
  list(type = "line", x0 = m, x1 = m, y0 = 0, y1 = 1, yref = "paper",
       line = list(color = "#D8D2C4", width = 1, dash = "dot"))
})

anotaciones_mes <- lapply(meses[-length(meses)], function(m) {
  list(x = m + 14, y = 1.06, yref = "paper", showarrow = FALSE,
       text = toupper(meses_es[as.integer(format(m, "%m"))]), font = list(size = 10, color = "#A39B8B", family = "Georgia"))
})

# Marcadores por hito: se dibujan en TRES llamadas separadas (una por tipo de hito),
# cada una usando el subconjunto real de datos de ese tipo, con color/símbolo/tamaño
# ya resueltos como valores literales (color_hex, symbol_val, size_px). Esto evita
# por completo que plotly tenga que inferir una paleta categórica —fuente del bug
# de colores/símbolos cruzados— y de paso genera automáticamente una leyenda
# correcta con las tres etiquetas (una por cada llamada/subconjunto).
tipos_presentes <- c("Proyecto de Ley", "Sesión Comisión", "Medida Ejecutiva")

for (tp in tipos_presentes) {
  datos_tp <- hitos %>% filter(tipo == tp)
  if (nrow(datos_tp) == 0) next

  fig <- fig %>% add_markers(
    data = datos_tp,
    x = ~fecha_inicio, y = ~y_pos,
    marker = list(
      color   = ~color_hex,
      symbol  = ~symbol_val,
      size    = ~size_px,
      line    = list(width = 1.5, color = "#FAF7F0"),
      opacity = 0.95
    ),
    customdata = ~url,
    name = tp,
    legendgroup = tp,
    showlegend = TRUE,
    text = ~paste0(
      "<b>", evento, "</b><br>",
      fecha_es(fecha_inicio),
      "<br><i>", tipo, "</i><br><br>",
      wrap_hover(detalle),
      ifelse(!is.na(url) & url != "", "<br><br>📰 <i>Clic para ver la nota de prensa</i>", "")
    ),
    hoverinfo = "text",
    hoverlabel = list(bgcolor = "#FAF7F0", font = list(family = "Georgia", size = 12, color = "#3B3B3B"),
                       bordercolor = "#D8D2C4")
  )
}

# Layout editorial: fondo cálido, tipografía serif, sin grilla vertical 
fig <- fig %>% layout(
  title = list(
    text = "<b>Agenda Educativa 2026</b><br><sup>Legislativo · Ejecutivo (marzo-julio)</sup>",
    font = list(family = "Georgia", size = 20, color = "#3B3B3B"),
    x = 0.02
  ),
  paper_bgcolor = "#FAF7F0",
  plot_bgcolor  = "#FAF7F0",
  font = list(family = "Georgia", color = "#3B3B3B"),
  xaxis = list(
    title = "", showgrid = FALSE, zeroline = FALSE,
    range = c("2026-03-18", "2026-07-31"),
    tickfont = list(size = 11)
  ),
  yaxis = list(
    title = "", showgrid = TRUE, gridcolor = "#EDE8DC",
    zeroline = FALSE, tickfont = list(size = 12),
    tickvals = c(1, 1.35),
    ticktext = c("Ejecutivo", "Legislativo"),
    range = c(0.8, 1.55)
  ),
  legend = list(orientation = "h", y = -0.15, font = list(size = 11)),
  margin = list(t = 90, b = 60),
  shapes = shapes_mes,
  annotations = anotaciones_mes
)

# Clic sobre un punto con nota de prensa asociada -> abre el link en pestaña nueva.
# Los puntos sin URL (campo vacío en el csv) simplemente no hacen nada al clic.
fig <- fig %>% onRender("
  function(el) {
    el.on('plotly_click', function(d) {
      var url = d.points[0].customdata;
      if (url && url !== '') {
        window.open(url, '_blank');
      }
    });
  }
")

fig

Notas de estilo

  • Símbolo por tipo, no solo color por ámbito: dentro de una misma fila (“Legislativo” concentra Proyecto de Ley y Sesión Comisión), el símbolo distingue a simple vista, por ejemplo, un proyecto de ley (◆) de una sesión de comisión (●), sin depender solo de leer el texto.
  • Tamaño uniforme (13px) para todos los marcadores, sin importar el tipo o ámbito del hito. Antes variaba según una “relevancia política” asignada manualmente (1-3), pero se eliminó por decisión del equipo al resultar un criterio demasiado subjetivo de estandarizar entre distintas personas.
  • Paleta cálida (azul petróleo / burdeos) en vez de los colores por defecto de plotly, para que combine mejor con un documento de análisis político y no se vea como un “dashboard corporativo”.
  • Al distribuir los hitos en dos filas (una por ámbito), se reduce bastante la superposición entre un hito Ejecutivo y uno Legislativo en la misma fecha (como el anuncio del SAE y el inicio de su tramitación, ambos el 23 de junio). Aun así, dos hitos del mismo ámbito muy cercanos en fecha pueden seguir superponiéndose; si eso empieza a pasar seguido, la salida es bajar el valor fijo de size_px (en el chunk {r paleta}) o aumentar fig.width/fig.height.
  • Las estelas también tienen su propio hover, mostrando el nombre del proceso, su rango de fechas y el detalle. Técnicamente se dibujan como una línea con un punto de datos cada 2 días (no solo inicio y fin) porque plotly solo activa el hover exactamente sobre un punto de datos real: con únicamente 2 puntos (como hace add_segments()), el tooltip solo aparecía pegado a los extremos de la banda. Si una estela se ve “entrecortada” en vez de una línea continua, es porque el rango de fechas es muy corto para el by = "2 days" fijado — se puede achicar ese intervalo en el chunk {r timeline-estilizada}.
  • Notas de prensa clickeables: los hitos que tienen una URL cargada en la columna url del csv muestran “📰 Clic para ver la nota de prensa” en el hover, y al hacer clic sobre el punto se abre el link en una pestaña nueva. Los hitos sin URL (campo vacío) no hacen nada al clic. No es necesario que todos los eventos tengan una nota asociada.

Próximos pasos

  • Externalizar hitos a un .csv compartido — hecho: ahora se lee desde hitos_educacion.csv (mantener la columna tipo para conservar la codificación visual; relevancia ya no afecta el gráfico, ver nota sobre tamaño uniforme).
  • Definir la utilidad del ámbito Prensa — resuelto: se eliminó por decisión del equipo (redundante con Ejecutivo/Legislativo); ya no existen filas de Prensa en el csv ni en el gráfico.
  • Al agregar un nuevo hito con rango de fechas (una nueva “estela”), en caso de querer que tenga un color propio distinto al de su ámbito, agregar a colores_rango en el chunk {r paleta} usando el nombre exacto del evento como llave; si no se agrega, la estela hereda el color de su ámbito por defecto.
  • Si en algún momento se agrega un nuevo tipo de hito (además de los tres actuales: Proyecto de Ley, Sesión Comisión, Medida Ejecutiva), hay que sumarlo en dos lugares del chunk {r paleta}: el case_when de symbol_val y la lista tipos_presentes del chunk del gráfico, o de lo contrario ese hito se dibuja con el símbolo por defecto (“circle”) y no aparece como su propia línea en la leyenda.
  • Seguir sumando hitos del Poder Ejecutivo, priorizando siempre dictámenes y cambios normativos emitidos desde el Ministerio de Educación y lo que subyace a él (Superintendencia, DEP, JUNAEB) por sobre anuncios sin traducción normativa.
  • Seguir sumando hitos del Poder Legislativo, incluyendo sesiones de Sala (no solo de comisión) del Senado y la Cámara en materia educativa, que según lo conversado es más fácil de sistematizar que Ejecutivo.
  • Evaluar un símbolo adicional (como puede ser un borde grueso o relleno hueco) para diferenciar indicaciones aprobadas de rechazadas, aprovechando el detalle que ya tienen sistematizado en los anexos de indicaciones.
  • Si el .Rmd se vuelve parte fija del flujo del Observatorio, considerar mover la paleta de colores (colores_ambito, simbolos_tipo) a un archivo tema_observatorio.R compartido, para mantener consistencia visual entre informes.