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.
Cada fila representa un hito, con los siguientes campos:
evento: título corto del hito (aparece en el
gráfico)ambito: Legislativo / Ejecutivofecha_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 conread_csv()en vez de mantenerla comotribble()dentro del.Rmd. Esto facilita que distintos integrantes del equipo la actualicen sin tocar el documento.
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"))
)
# `detalle_estela` es una descripción específica para la estela (el conjunto del
# proceso), distinta de `detalle` (que describe el evento puntual). Si el csv no
# tiene esta columna (por ejemplo, una versión antigua sin actualizar), o si está
# vacía para alguna fila, se usa `detalle` como respaldo, para que nada quede sin texto.
if (!"detalle_estela" %in% names(hitos)) {
hitos$detalle_estela <- NA_character_
}
hitos <- hitos %>%
mutate(detalle_estela = ifelse(is.na(detalle_estela) | detalle_estela == "", detalle, detalle_estela))
hitos
## # A tibble: 15 × 10
## evento ambito tipo relevancia fecha_inicio fecha_fin detalle url
## <chr> <fct> <chr> <dbl> <date> <date> <chr> <chr>
## 1 Ingreso 'Escue… Legis… Proy… 3 2026-04-07 2026-06-12 Proyec… <NA>
## 2 Aprobación gen… Legis… Proy… 2 2026-05-05 2026-05-05 Aproba… <NA>
## 3 Sesión 13 Comi… Legis… Sesi… 2 2026-05-12 2026-05-12 Votaci… <NA>
## 4 Sesión 14: Adm… Legis… Sesi… 3 2026-06-02 2026-06-02 Aproba… <NA>
## 5 Sesión 15: cri… Legis… Sesi… 3 2026-06-09 2026-06-09 Denunc… <NA>
## 6 Oficio Circula… Ejecu… Medi… 3 2026-04-21 2026-04-21 Hacien… <NA>
## 7 Promulgación L… Legis… Proy… 3 2026-03-25 2026-03-25 Marco … http…
## 8 Resolución Exe… Ejecu… Medi… 3 2026-04-27 2026-04-27 Gobier… <NA>
## 9 TGR inicia emb… Ejecu… Medi… 1 2026-04-23 2026-04-23 Primer… <NA>
## 10 TGR amplía emb… Ejecu… Medi… 3 2026-06-04 2026-06-04 Segund… <NA>
## 11 Comisión Educa… Legis… Sesi… 1 2026-04-15 2026-04-15 El min… http…
## 12 Kast ingresa i… Legis… Proy… 3 2026-06-15 2026-06-15 Boletí… http…
## 13 Comisión Educa… Legis… Sesi… 2 2026-07-08 2026-07-08 La Com… http…
## 14 Anuncio reform… Ejecu… Medi… 2 2026-06-23 2026-06-23 Kast a… http…
## 15 Tramitación re… Legis… Proy… 3 2026-06-23 2026-07-23 Ingres… http…
## # ℹ 2 more variables: detalle_estela <chr>, es_rango <lgl>
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" = "#003F6E",
"Ejecutivo" = "#81C5F7"
)
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"
)
# Etiqueta corta para la leyenda (el nombre completo del evento es muy largo
# para mostrarlo tal cual ahí). Si un evento nuevo con rango de fechas no tiene
# etiqueta corta definida acá, se usa su nombre completo como respaldo.
etiquetas_rango <- c(
"Ingreso 'Escuelas Protegidas' (18156-04)" = "Escuelas Protegidas (tramitación)",
"Tramitación reforma al SAE (Elección Mutua)" = "Reforma SAE (tramitación)"
)
etiqueta_estela <- function(evento) {
if (evento %in% names(etiquetas_rango)) etiquetas_rango[[evento]] else evento
}
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")
# Se usa "detalle_estela" (descripción del proceso completo) si existe;
# si una estela nueva todavía no la tiene definida en el csv, cae de
# respaldo al "detalle" del evento puntual que la origina.
descripcion_estela <- if (!is.na(rangos$detalle_estela[i]) && rangos$detalle_estela[i] != "") {
rangos$detalle_estela[i]
} else {
rangos$detalle[i]
}
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(descripcion_estela)
)
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,
name = etiqueta_estela(rangos$evento[i]),
legendgroup = etiqueta_estela(rangos$evento[i]),
showlegend = TRUE,
text = rep(texto_rango, length(fechas_rango)),
hoverinfo = "text",
hoverlabel = list(bgcolor = "#FFF6E3", font = list(family = "Titillium Web", size = 12, color = "#3B3B3B"),
bordercolor = "#D8D2C4")
)
}
# Etiquetas de mes (texto grande arriba: MARZO, ABRIL, etc.) — se mantienen
# como referencia general de orientación.
meses <- seq(as.Date("2026-03-01"), as.Date("2026-08-01"), by = "month")
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 = "Titillium Web"))
})
# Líneas verticales punteadas: antes marcaban el inicio de cada MES, ahora marcan
# el inicio de cada SEMANA (una línea cada 7 días desde el 1 de marzo).
semanas <- seq(as.Date("2026-03-01"), as.Date("2026-08-01"), by = "week")
shapes_mes <- lapply(semanas, function(s) {
list(type = "line", x0 = s, x1 = s, y0 = 0, y1 = 1, yref = "paper",
line = list(color = "#D8D2C4", width = 1, dash = "dot"))
})
# 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 = "#FFF6E3", font = list(family = "Titillium Web", 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 = "Titillium Web", size = 20, color = "#3B3B3B"),
x = 0.02
),
paper_bgcolor = "#FFF6E3",
plot_bgcolor = "#FFF6E3",
font = list(family = "Titillium Web", 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
Este .Rmd completo (con notas metodológicas, tablas,
etc.) sirve como documentación interna del equipo. Para la página web
solo se necesita el widget interactivo, exportado como
un único archivo .html autocontenido (con todo su
JavaScript incluido adentro), listo para insertar mediante un
<iframe>.
El siguiente chunk está desactivado por defecto
(eval=FALSE), para que no se ejecute cada vez que alguien
del equipo haga knit del reporte completo. Para generar o
actualizar el archivo, corre esta línea manualmente en la consola de
RStudio (con el resto del documento ya ejecutado, para que
fig exista en el ambiente):
# Ejecutar manualmente cuando se quiera (re)generar el archivo para la web.
# selfcontained = TRUE empaqueta todo (JS/CSS) en un solo .html, sin depender
# de ningún archivo externo — ideal para subir directo o pasarle al equipo web.
# "Titillium Web" es una Google Font: si no está instalada en el computador de
# quien mire el gráfico, el navegador la ignora silenciosamente y usa una fuente
# genérica en su lugar. Para que se vea igual en cualquier dispositivo, se inyecta
# acá el link de Google Fonts directamente en el <head> del html exportado
# (esto SÍ requiere conexión a internet para cargar la fuente al abrir la página,
# aunque el resto del gráfico funcione sin conexión gracias a selfcontained=TRUE).
fig_para_web <- fig %>%
htmlwidgets::prependContent(
htmltools::tags$link(
rel = "stylesheet",
href = "https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap"
)
)
htmlwidgets::saveWidget(
fig_para_web,
file = "grafico_hitos_educacion.html",
selfcontained = TRUE,
title = "Agenda Educativa 2026"
)
Con el archivo grafico_hitos_educacion.html ya generado,
en el sitio del centro de estudios se incrusta así (ajustando alto/ancho
a su plantilla):
<iframe src="grafico_hitos_educacion.html"
width="100%" height="550"
style="border:none;">
</iframe>
Dos cosas a tener en cuenta:
selfcontained = TRUE, el archivo pesa varios MB
(porque incluye la librería completa de plotly.js adentro)
— normal para un solo gráfico interactivo, pero si el sitio tiene un
límite estricto de tamaño de archivo, se puede usar
selfcontained = FALSE en su lugar; en ese caso
saveWidget() genera además una carpeta
grafico_hitos_educacion_files/ con las dependencias, que
también hay que subir junto al .html (no solo el archivo
suelto).hitos_educacion.csv con
nuevos hitos, hay que volver a correr este chunk para regenerar el
.html y volver a subirlo — no se actualiza solo.plotly, para que combine mejor
con un documento de análisis político y no se vea como un “dashboard
corporativo”.size_px (en el chunk {r paleta}) o aumentar
fig.width/fig.height.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}.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.hitos a un .csv
compartidohitos_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).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. La estela ya
aparece en la leyenda automáticamente (con su nombre completo como
texto); si prefieres un texto más corto para la leyenda, agrégalo
también a etiquetas_rango con la misma llave. Además,
conviene llenar la columna detalle_estela en el csv con una
descripción genérica del proceso completo (no del evento puntual que lo
origina) — si se deja vacía, el hover de la estela cae de respaldo al
detalle normal de esa fila.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..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.