Copia y pega el siguiente código en tu proyecto de Python para generar una gráfica SVG con estilo personalizado.
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def agrupadasyapiladas(dataframes):
"""
Genera una gráfica de barras apiladas agrupadas para múltiples DataFrames.
Args:
dataframes (list of pd.DataFrame): Lista de DataFrames con los datos de los grupos de subgrupos apilados.
"""
font_config = {
'family': 'Arial',
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 14, 'weight': 'medium', 'color': '#767676'}
}
# Configuración de los grupos y subgrupos
grupos = dataframes[0].index.tolist()
subgrupos_list = [df.columns.tolist() for df in dataframes]
# Obtener colores desde la función colores()
lista_colores = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162", "#10302C", "#E6D194", "#018477", "#FF6666", "#00008B", "#854991"]
colores_por_grupo = []
color_index = 0
for subgrupos in subgrupos_list:
colores_por_grupo.append(lista_colores[color_index:color_index + len(subgrupos)])
color_index += len(subgrupos)
# Configuración visual
factor_ancho = 0.4
ancho_total = len(grupos) * factor_ancho
max_subgrupos = max(len(df.columns) for df in dataframes)
ancho_barra = ancho_total / (len(dataframes) * max_subgrupos)
desplazamiento = -ancho_total / 2 + ancho_barra / 2
# Configurar el tamaño de la figura en píxeles
ancho_px = 1480
alto_px = 520
dpi = 100
ancho_in = ancho_px / dpi
alto_in = alto_px / dpi
fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
x_pos = np.arange(len(grupos))
for df, colores, offset in zip(dataframes, colores_por_grupo, range(len(dataframes))):
bottom = np.zeros(len(grupos))
total_grupo = df.sum(axis=1)
for subgrupo, color in zip(df.columns, colores):
valores = df[subgrupo]
ax.bar(
x_pos + desplazamiento,
valores,
width=ancho_barra,
bottom=bottom,
color=color,
label=subgrupo,
)
for i, valor in enumerate(valores):
porcentaje = (valor / total_grupo.iloc[i]) * 100 if total_grupo.iloc[i] != 0 else 0
ax.text(
x_pos[i] + desplazamiento,
bottom[i] + valor / 2,
f"{int(valor):,} ({porcentaje:.1f}%)",
ha='center',
va='center',
fontsize=font_config['capsula_valor']['size'],
fontweight=font_config['capsula_valor']['weight'],
color='white' if porcentaje > 10 else 'black'
)
bottom += np.array(valores)
factor_separacion = max(bottom) * 0.05
for i, total in enumerate(bottom):
ax.text(
x_pos[i] + desplazamiento,
total + factor_separacion,
f"{int(total):,}",
ha="center",
va="bottom",
fontsize=font_config['capsula_max']['size'],
fontweight=font_config['capsula_max']['weight'],
color=colores[1] if len(colores) > 1 else 'black',
bbox=dict(boxstyle="round,pad=0.35,rounding_size=0.99", fc="white", ec=colores[0], alpha=1.0)
)
desplazamiento += ancho_barra
ax.legend(
loc='upper center',
bbox_to_anchor=(0.5, 1.5),
frameon=False,
prop={'size': font_config['leyenda']['size'], 'weight': font_config['leyenda']['weight']},
ncol=max(1, len(dataframes)),
)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(True)
ax.spines['bottom'].set_linewidth(2)
ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{int(x):,}"))
ax.tick_params(axis='y', labelsize=font_config['eje_y']['size'], labelcolor=font_config['eje_y']['color'])
ax.yaxis.label.set_size(font_config['eje_y']['size'])
ax.yaxis.label.set_weight(font_config['eje_y']['weight'])
ax.yaxis.label.set_color(font_config['eje_y']['color'])
ax.set_xticks(x_pos)
ax.set_xticklabels(grupos, fontsize=font_config['eje_x']['size'], fontweight=font_config['eje_x']['weight'], color=font_config['eje_x']['color'])
ax.set_ylabel('Valores', fontsize=font_config['etiquetas_eje_y']['size'], fontweight=font_config['etiquetas_eje_y']['weight'], color=font_config['etiquetas_eje_y']['color'])
plt.tight_layout()
plt.savefig("grafica_agrupadasyapiladas.svg", format="svg", bbox_inches='tight', dpi=dpi)
plt.show()
# Llamar a la función con una lista de DataFrames
agrupadasyapiladas(dataframes_a_graficar)
Ver carpeta en GitHub
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from scipy.interpolate import make_interp_spline
# VARIABLES A CAMBIAR:
dataframe_a_graficar = data
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def areaplot(data):
"""
Genera un gráfico de área apilada con los datos proporcionados en un DataFrame.
Parámetros:
- data: DataFrame que contiene las columnas 'fechas', 'comisiones', 'fiscalias' y 'portal'.
"""
font_config = {
'family': 'Arial',
'titulo': {'size': 16, 'weight': 'bold', 'color': '#10302C'},
'eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 20, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}
}
# Extraer columnas del DataFrame
fechas = data['fechas']
comisiones = data['comisiones']
fiscalias = data['fiscalias']
portal = data['portal']
# Crear figura
fig, ax = plt.subplots(figsize=(16, 8))
n = len(fechas)
x = np.arange(n) + 0.5
# Suavizar las curvas visualmente
x_suave = np.linspace(x.min(), x.max(), 300)
comisiones_suave = make_interp_spline(x, comisiones)(x_suave)
fiscalias_suave = make_interp_spline(x, fiscalias)(x_suave)
portal_suave = make_interp_spline(x, portal)(x_suave)
# Área apilada
ax.stackplot(x_suave, comisiones_suave, fiscalias_suave, portal_suave, labels=[
'Comisiones', 'Fiscalías y Procuradurías', 'Portal'],
colors=['#215F53', '#7570B3', '#C7EAE5'])
# Eje X
ax.set_xticks(x)
ax.set_xticklabels(fechas, rotation=90, fontsize=font_config['eje_x']['size'],
fontweight=font_config['eje_x']['weight'], color=font_config['eje_x']['color'])
ax.set_xlim(x[0] - 0.5, x[-1] + 0.5)
# Eje Y
ax.set_yticks(np.linspace(0, 1, 5))
ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=font_config['eje_y']['size'],
fontweight=font_config['eje_y']['weight'], color=font_config['eje_y']['color'])
ax.set_ylabel('Porcentaje', fontsize=font_config['eje_y']['size'],
fontweight=font_config['eje_y']['weight'], color=font_config['eje_y']['color'])
# Bordes y grid
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(True)
ax.spines['bottom'].set_linewidth(2)
ax.grid(axis='y', linestyle='-', color='white', alpha=0.8, linewidth=1.25)
ax.grid(axis='x', linestyle='-', color='white', alpha=0.3, linewidth=0.75)
# Coordenadas y valores totales por capa
x_coords = [2, 8, 14]
y_coords = [0.15, 0.45, 0.85]
totales = [round(sum(comisiones), 2), round(sum(fiscalias), 2), round(sum(portal), 2)]
colores_borde = ['#D9D9D9'] * 3
for x, y, total, color_borde in zip(x_coords, y_coords, totales, colores_borde):
ax.text(
x, y, f"{total:,}",
ha='center', va='center',
fontsize=font_config['capsula_valor']['size'],
fontweight=font_config['capsula_valor']['weight'],
color=font_config['capsula_valor']['color'],
bbox=dict(
boxstyle="round,pad=0.6,rounding_size=0.99",
facecolor='white',
edgecolor=color_borde,
linewidth=1.8
)
)
# Leyenda centrada arriba con tamaño ajustado
ax.legend(
loc='upper center',
bbox_to_anchor=(0.5, 1.12),
ncol=3,
frameon=False,
prop=font_manager.FontProperties(
size=font_config['leyenda']['size'],
weight=font_config['leyenda']['weight']
)
)
plt.tight_layout()
plt.savefig("areaplot.svg", format="svg", bbox_inches='tight')
plt.show()
areaplot(dataframe_a_graficar)
Ver carpeta en GitHub
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
import matplotlib.ticker as mticker
from adjustText import adjust_text
# VARIABLES A CAMBIAR
dataframe_a_graficar = df
columna_eje_x = "Fecha"
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def ajusta_etiquetas_apiladas_manual(dataframe, columnas, colores, columna_x, sin_tag=1, etiquetar_max=True, bbox_props=None, fontsize=12, separacion=40):
"""
Ajusta las etiquetas de los puntos en una gráfica apilada, colocándolas por encima del punto más alto
y apilándolas verticalmente en orden correcto.
Args:
dataframe (pd.DataFrame): DataFrame con los datos.
columnas (list): Columnas a etiquetar.
colores (list): Colores para cada etiqueta.
columna_x (str): Columna para el eje X.
sin_tag (int): Puntos sin etiqueta entre etiquetas. Default: 2.
etiquetar_max (bool): Si True, etiqueta el valor máximo de cada columna. Default: True.
bbox_props (dict): Propiedades del fondo de la etiqueta. Default: None (estilo predeterminado).
fontsize (int): Tamaño de fuente. Default: 12.
separacion (int): Separación vertical entre etiquetas (en unidades Y). Default: 40.
"""
# Calcular la altura acumulada máxima en cada punto X
acumulado = dataframe[columnas].cumsum(axis=1)
y_max_global = acumulado.max(axis=1) # Altura máxima en cada X
# Diccionario para guardar la posición Y más alta ocupada en cada X
y_max_por_x = {}
colores = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162"]
for col, color in zip(columnas, colores):
total_puntos = len(dataframe)
max_index = dataframe[col].idxmax() if etiquetar_max else None # Índice del máximo si etiquetar_max=True
for i, row in dataframe.iterrows():
x_pos = row[columna_x] # Posición X actual
tiene_etiqueta = (
(total_puntos - i - 1) % (sin_tag + 1) == 0 # Cada 'sin_tag' puntos
or i == total_puntos - 1 # Siempre etiquetar el último punto
or (etiquetar_max and i == max_index) # Etiquetar el máximo si etiquetar_max=True
)
if tiene_etiqueta:
# Altura base: el punto más alto en esta posición X
y_base = y_max_global[i] - 200 # Desplazar todas las etiquetas hacia abajo (ajusta el valor -200 según sea necesario)
# Si ya hay una etiqueta en esta X, apilarla encima de la última
if x_pos in y_max_por_x:
y_etiqueta = y_max_por_x[x_pos] + separacion
else:
y_etiqueta = y_base + separacion # Espacio inicial por encima del punto más alto
# Actualizar la altura máxima registrada para esta X
y_max_por_x[x_pos] = y_etiqueta
# Configurar el fondo de la etiqueta para puntos intermedios
if i != 1 and i != total_puntos - 1: # Puntos intermedios
bbox_props_intermedio = dict(boxstyle="round,pad=0.3,rounding_size=0.99", fc="white", ec="gray", alpha=1.0)
texto_color = color # Mismo color que la gráfica
else: # Puntos máximos o finales
bbox_props_intermedio = bbox_props or dict(boxstyle="round,pad=0.3,rounding_size=0.99", fc=color, ec="none", alpha=1.0)
texto_color = "white" # Texto blanco para máximos/finales
# Añadir la etiqueta
plt.text(
x_pos, # Posición X (misma que la barra)
y_etiqueta, # Posición Y (encima del punto más alto/apilada)
f"{int(row[col]):,}",
fontsize=fontsize,
color=texto_color, # Color del texto
ha='center',
va='bottom',
bbox=bbox_props_intermedio
)
def areaplot2(df, columna_fecha, tipografia=None):
"""
Genera una gráfica de área (area plot) a partir de un DataFrame.
Args:
df (pd.DataFrame): DataFrame que contiene los datos a graficar.
columna_fecha (str): Nombre de la columna que se usará como eje X.
tipografia (FontProperties, optional): Fuente personalizada para los textos. Por defecto es None.
"""
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
# Verificar que la columna especificada exista en el DataFrame
if columna_fecha not in df.columns:
raise ValueError(f"La columna '{columna_fecha}' no existe en el DataFrame.")
# Configurar el tamaño de la figura en píxeles
ancho_px = 1480
alto_px = 520
dpi = 100 # Resolución en píxeles por pulgada
ancho_in = ancho_px / dpi
alto_in = alto_px / dpi
# Crear la figura con el tamaño especificado
fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
# Obtener los colores
colores_asignados = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162"]
# Obtener las columnas a graficar (excluyendo la columna de fecha)
columnas_a_graficar = [col for col in df.columns if col != columna_fecha]
# Graficar el área para cada columna
ax.stackplot(
df[columna_fecha],
[df[col] for col in columnas_a_graficar],
labels=columnas_a_graficar,
colors=colores_asignados[:len(columnas_a_graficar)],
alpha=0.8
)
# Configurar etiquetas y título
ax.set_xlabel(columna_fecha, fontdict=font_config['etiquetas_eje_x'])
ax.set_ylabel("Valores", fontdict=font_config['etiquetas_eje_y'])
# Configurar las etiquetas del eje X
ax.tick_params(axis='x', labelsize=font_config['etiquetas_eje_x']['size'], labelcolor=font_config['etiquetas_eje_x']['color'])
ax.tick_params(axis='y', labelsize=font_config['etiquetas_eje_y']['size'], labelcolor=font_config['etiquetas_eje_y']['color'])
# Posicionar el eje X en y=0
ax.spines['bottom'].set_position(('data', 0))
# Rotar las etiquetas del eje X
plt.xticks(rotation=90)
# Desactivar o activar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(True)
# Asignar grosor a los ejes visibles
ax.spines['bottom'].set_linewidth(2) # Grosor del eje inferior
# Mantener las líneas del grid
ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
ax.grid(axis='x', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
# Ajustar etiquetas en los puntos máximos de cada columna
ajusta_etiquetas_apiladas_manual(
dataframe=df,
columnas=columnas_a_graficar,
colores=colores_asignados,
columna_x=columna_fecha,
fontsize=font_config['capsula_valor']['size'],
separacion=1100
)
# Calcular el valor máximo de las etiquetas y agregar un margen
y_max = df[columnas_a_graficar].sum(axis=1).max() # Suma acumulada máxima
margen = y_max * 0.5 # Margen del 10%
ax.set_ylim(0, y_max + margen) # Ajustar el límite del eje Y
# Guardar la gráfica como archivo SVG
#plt.savefig("areaplot2.png", format="png", bbox_inches='tight', dpi=dpi)
plt.savefig("areaplot2.svg", format="svg", bbox_inches='tight', dpi=dpi)
# Llamar a la función con el DataFrame df, la columna 'Fecha' y la fuente personalizada
areaplot2(dataframe_a_graficar, columna_fecha=columna_eje_x, tipografia=arial_font)
Ver carpeta en GitHub
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
# VARIABLES A CAMBIAR:
dataframe_a_graficar = data
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def calcular_intervalo(max_valor):
"""Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
potencia = 10 ** (int(np.log10(max_valor)) - 1)
intervalo = round(max_valor / 5 / potencia) * potencia
# Asegurar que el intervalo no sea muy pequeño o grande
if max_valor / intervalo > 8:
intervalo *= 2
elif max_valor / intervalo < 4:
intervalo /= 2
return max(intervalo, 1) # Mínimo intervalo de 1
def barras_apiladas_horizontales(data_vertical, bar_width=0.6):
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
categorias = ['Hombre', 'Mujer', 'No identificado']
data['Total'] = data[categorias].sum(axis=1)
sorted_data = data.sort_values(by='Total', ascending=False)
entidades = sorted_data.index.tolist()
max_valor = sorted_data['Total'].max()
total = sorted_data['Total'].sum()
x_max = max_valor * 1.15
x_interval = max_valor // 5 or 1
fig, ax = plt.subplots(figsize=(10, 14), dpi=300)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
x_ticks = np.arange(0, x_max + x_interval, x_interval)
for x in x_ticks:
lw = 2 if x == max_valor else 0.75
lc = 'black' if x == max_valor else '#B9B9B9'
ax.axvline(x, color=lc, linewidth=lw)
ax.axvline(0, color='black', linewidth=2, zorder=1)
offset_capsula = x_max * 0.015
offset_porcentaje = x_max * 0.09
bar_positions = np.arange(len(entidades))
colores = {
'Hombre': '#4C6A67',
'Mujer': '#627B78',
'No identificado': '#6F8583'
}
categorias = list(colores.keys());
for i, entidad in enumerate(entidades):
row = sorted_data.loc[entidad]
y_pos = bar_positions[i]
x_left = 0
for cat in categorias:
valor = row[cat]
ax.barh(y_pos, valor, height=bar_width, left=x_left,
color=colores[cat], edgecolor='none', zorder=2)
x_left += valor
total_valor = row['Total']
porcentaje = round((total_valor / total) * 100, 1)
texto_kwargs = dict(
ha='left',
va='center',
fontsize=font_config['capsula_valor']['size'],
fontfamily=font_config['family'],
fontweight=font_config['capsula_valor']['weight'],
color=font_config['capsula_valor']['color']
)
if total_valor <= 5:
boxstyle = "round,pad=0.4,rounding_size=0.59"
elif total_valor <= 50:
boxstyle = "round,pad=0.3,rounding_size=0.7"
elif total_valor <= 100:
boxstyle = "round,pad=0.3,rounding_size=0.79"
else:
boxstyle = "round,pad=0.3,rounding_size=0.9"
bbox_props = dict(
boxstyle=boxstyle,
facecolor='white',
edgecolor='#002F2A',
linewidth=1.5
)
ax.text(
total_valor + offset_capsula, y_pos, f"{int(total_valor):,}",
bbox=bbox_props,
**texto_kwargs
)
ax.text(
total_valor + offset_porcentaje, y_pos, f"{porcentaje}%",
color=font_config['porcentaje']['color'],
fontsize=font_config['porcentaje']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje']['weight'],
ha='left',
va='center'
)
ax.set_yticks(bar_positions)
ax.set_yticklabels(
entidades,
fontsize=font_config['eje_y']['size'],
fontfamily=font_config['family'],
fontweight=font_config['eje_y']['weight']
)
ax.set_xlim(0, x_max)
ax.invert_yaxis()
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(
axis='x',
labelsize=font_config['eje_x']['size'], # Tamaño de la fuente
labelcolor=font_config['eje_x']['color'] # Color de la fuente
)
ax.grid(False)
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("barras_apiladas_horizontales.svg", format="svg", bbox_inches='tight')
plt.show()
barras_apiladas_horizontales(dataframe_a_graficar)
Ver carpeta en GitHub
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
# VARIABLES A CAMBIAR:
dataframe_a_graficar = data
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def calcular_intervalo(max_valor):
"""Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
potencia = 10 ** (int(np.log10(max_valor)) - 1)
intervalo = round(max_valor / 5 / potencia) * potencia
# Asegurar que el intervalo no sea muy pequeño o grande
if max_valor / intervalo > 8:
intervalo *= 2
elif max_valor / intervalo < 4:
intervalo /= 2
return max(intervalo, 1) # Mínimo intervalo de 1
def barras_apiladas_verticales(data, bar_width=0.6):
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 12, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
categorias = ['Hombre', 'Mujer', 'No identificado']
data['Total'] = data[categorias].sum(axis=1)
sorted_data = data.sort_values(by='Total', ascending=False)
entidades = sorted_data.index.tolist()
max_valor = sorted_data['Total'].max()
total = sorted_data['Total'].sum()
y_max = max_valor * 1.15
y_interval = calcular_intervalo(max_valor)
fig, ax = plt.subplots(figsize=(16, 6), dpi=300)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
y_ticks = np.arange(0, y_max + y_interval, y_interval)
for y in y_ticks:
lw = 2 if y == y_max else 0.75
lc = 'black' if y == y_max else '#B9B9B9'
ax.axhline(y, color=lc, linewidth=lw)
ax.axhline(0, color='black', linewidth=2, zorder=1)
offset_capsula = y_max * 0.05
offset_porcentaje = y_max * 0.15
bar_positions = np.arange(len(entidades))
colores = {
'Hombre': '#4C6A67',
'Mujer': '#627B78',
'No identificado': '#6F8583'
}
categorias = list(colores.keys());
for i, entidad in enumerate(entidades):
row = sorted_data.loc[entidad]
x_pos = bar_positions[i]
y_bottom = 0
for cat in categorias:
valor = row[cat]
ax.bar(x_pos, valor, width=bar_width, bottom=y_bottom,
color=colores[cat], edgecolor='none', zorder=2)
y_bottom += valor
total_valor = row['Total']
porcentaje = round((total_valor / total) * 100, 1)
# Estilo de texto
texto_kwargs = dict(
ha='center',
va='bottom',
fontsize=font_config['capsula_valor']['size'],
fontfamily=font_config['family'],
fontweight=font_config['capsula_valor']['weight'],
color=font_config['capsula_valor']['color']
)
# Ajuste de estilo de cápsula según el valor
if total_valor <= 5:
boxstyle = "round,pad=0.4,rounding_size=0.59"
elif total_valor <= 50:
boxstyle = "round,pad=0.3,rounding_size=0.7"
elif total_valor <= 100:
boxstyle = "round,pad=0.3,rounding_size=0.79"
else:
boxstyle = "round,pad=0.3,rounding_size=0.9"
bbox_props = dict(
boxstyle=boxstyle,
facecolor='white',
edgecolor='#002F2A',
linewidth=1.5
)
# Cápsula con total
ax.text(
x_pos, total_valor + offset_capsula, f"{int(total_valor):,}",
bbox=bbox_props,
**texto_kwargs
)
# Porcentaje encima
ax.text(
x_pos, total_valor + offset_porcentaje, f"{porcentaje}%",
color=font_config['porcentaje']['color'],
fontsize=font_config['porcentaje']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje']['weight'],
ha='center',
va='bottom'
)
# Eje X
ax.set_xticks(bar_positions)
ax.set_xticklabels(
entidades, rotation=90,
fontsize=font_config['eje_x']['size'],
fontfamily=font_config['family'],
fontweight=font_config['eje_x']['weight']
)
ax.set_ylim(0, y_max)
ax.set_xlim(bar_positions[0] - bar_width, bar_positions[-1] + bar_width)
# Limpiar bordes y ticks
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(axis='both', which='both', length=0)
ax.grid(False)
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("barras_apiladas_verticales.svg", format="svg", bbox_inches='tight')
plt.show()
barras_apiladas_verticales(dataframe_a_graficar)
Ver carpeta en GitHub
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def calcular_intervalo(max_valor):
"""Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
potencia = 10 ** (int(np.log10(max_valor)) - 1)
intervalo = round(max_valor / 5 / potencia) * potencia
# Asegurar que el intervalo no sea muy pequeño o grande
if max_valor / intervalo > 8:
intervalo *= 2
elif max_valor / intervalo < 4:
intervalo /= 2
return max(intervalo, 1) # Mínimo intervalo de 1
def barras_horizontales(victimas_por_entidad, bar_height=0.6):
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
# Ordenar de mayor a menor
sorted_items = sorted(victimas_por_entidad.items(), key=lambda x: x[1], reverse=True)
entidades = [item[0] for item in sorted_items]
valores = [item[1] for item in sorted_items]
max_valor = max(valores)
total = sum(valores)
y_positions = np.arange(len(entidades))
fig, ax = plt.subplots(figsize=(10, 14), dpi=300)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
x_max = max_valor * 1.5
x_interval = max_valor // 5 or 1
# Líneas verticales
x_ticks = np.arange(0, x_max + x_interval, x_interval)
for x in x_ticks:
lw = 2 if x == max_valor else 0.75
lc = 'black' if x == max_valor else '#B9B9B9'
ax.axvline(x, color=lc, linewidth=lw)
# Dibujar barras
for y_pos, entidad, valor in zip(y_positions, entidades, valores):
porcentaje = round((valor / total) * 100, 1)
if valor == max_valor:
for alpha in np.linspace(0.3, 1, 10):
ax.barh(y_pos, valor, height=bar_height,
color='#8B0000', alpha=alpha, edgecolor='none', zorder=2)
bbox_color = '#8B0000'
text_color = font_config['capsula_max']['color']
edge_col = 'none'
lw = 0
else:
for alpha in np.linspace(0.1, 0.8, 10):
ax.barh(y_pos, valor, height=bar_height,
color='#10302C', alpha=alpha, edgecolor='none', zorder=2)
bbox_color = 'white'
text_color = font_config['capsula_valor']['color']
edge_col = '#002F2A'
lw = 1.5
# Cápsula con valor
ax.text(
total_valor + offset_capsula, y_pos, f"{valor:,}",
bbox=dict(boxstyle="round,pad=0.3,rounding_size=0.7",
facecolor=bbox_color,
edgecolor=edge_col,
linewidth=lw),
ha='left',
va='center',
fontsize=font_config['capsula_valor']['size'],
fontfamily=font_config['family'],
fontweight=font_config['capsula_valor']['weight'],
color=text_color
)
# Porcentaje
ax.text(
total_valor + offset_porcentaje, y_pos, f"{porcentaje}%",
color=font_config['porcentaje']['color'],
fontsize=font_config['porcentaje']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje']['weight'],
ha='left',
va='center'
)
# Eje Y (etiquetas)
ax.set_yticks(y_positions)
ax.set_yticklabels(
entidades,
fontsize=font_config['eje_y']['size'],
fontfamily=font_config['family'],
fontweight=font_config['eje_y']['weight']
)
ax.set_xlim(0, x_max)
ax.invert_yaxis()
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(
axis='x',
labelsize=font_config['eje_x']['size'], # Tamaño de la fuente
labelcolor=font_config['eje_x']['color'] # Color de la fuente
)
ax.grid(False)
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("barras_horizontales.svg", format="svg", bbox_inches='tight')
plt.show()
barras_horizontales(dataframe_a_graficar)
Ver carpeta en GitHub
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def calcular_intervalo(max_valor):
"""Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
potencia = 10 ** (int(np.log10(max_valor)) - 1)
intervalo = round(max_valor / 5 / potencia) * potencia
# Asegurar que el intervalo no sea muy pequeño o grande
if max_valor / intervalo > 8:
intervalo *= 2
elif max_valor / intervalo < 4:
intervalo /= 2
return max(intervalo, 1) # Mínimo intervalo de 1
def barras_verticales(victimas_por_entidad, bar_width=0.6):
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 12, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 12, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
categorias = ['Hombre', 'Mujer', 'No identificado']
data['Total'] = data[categorias].sum(axis=1)
sorted_data = data.sort_values(by='Total', ascending=False)
entidades = sorted_data.index.tolist()
max_valor = sorted_data['Total'].max()
total = sorted_data['Total'].sum()
y_max = max_valor * 1.15
y_interval = calcular_intervalo(max_valor)
fig, ax = plt.subplots(figsize=(16, 6), dpi=300)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
y_ticks = np.arange(0, y_max + y_interval, y_interval)
for y in y_ticks:
lw = 2 if y == y_max else 0.75
lc = 'black' if y == y_max else '#B9B9B9'
ax.axhline(y, color=lc, linewidth=lw)
ax.axhline(0, color='black', linewidth=2, zorder=1)
offset_capsula = y_max * 0.05
offset_porcentaje = y_max * 0.15
bar_positions = np.arange(len(entidades))
colores = {
'Hombre': '#4C6A67',
'Mujer': '#627B78',
'No identificado': '#6F8583'
}
categorias = list(colores.keys());
for i, entidad in enumerate(entidades):
row = sorted_data.loc[entidad]
x_pos = bar_positions[i]
y_bottom = 0
for cat in categorias:
valor = row[cat]
ax.bar(x_pos, valor, width=bar_width, bottom=y_bottom,
color=colores[cat], edgecolor='none', zorder=2)
y_bottom += valor
total_valor = row['Total']
porcentaje = round((total_valor / total) * 100, 1)
# Estilo de texto
texto_kwargs = dict(
ha='center',
va='bottom',
fontsize=font_config['capsula_valor']['size'],
fontfamily=font_config['family'],
fontweight=font_config['capsula_valor']['weight'],
color=font_config['capsula_valor']['color']
)
# Ajuste de estilo de cápsula según el valor
if total_valor <= 5:
boxstyle = "round,pad=0.4,rounding_size=0.59"
elif total_valor <= 50:
boxstyle = "round,pad=0.3,rounding_size=0.7"
elif total_valor <= 100:
boxstyle = "round,pad=0.3,rounding_size=0.79"
else:
boxstyle = "round,pad=0.3,rounding_size=0.9"
bbox_props = dict(
boxstyle=boxstyle,
facecolor='white',
edgecolor='#002F2A',
linewidth=1.5
)
# Cápsula con total
ax.text(
x_pos, total_valor + offset_capsula, f"{int(total_valor):,}",
bbox=bbox_props,
**texto_kwargs
)
# Porcentaje encima
ax.text(
x_pos, total_valor + offset_porcentaje, f"{porcentaje}%",
color=font_config['porcentaje']['color'],
fontsize=font_config['porcentaje']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje']['weight'],
ha='center',
va='bottom'
)
# Eje X
ax.set_xticks(bar_positions)
ax.set_xticklabels(
entidades, rotation=90,
fontsize=font_config['eje_x']['size'],
fontfamily=font_config['family'],
fontweight=font_config['eje_x']['weight']
)
ax.set_ylim(0, y_max)
ax.set_xlim(bar_positions[0] - bar_width, bar_positions[-1] + bar_width)
# Limpiar bordes y ticks
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(axis='both', which='both', length=0)
ax.grid(False)
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("barras_verticales.svg", format="svg", bbox_inches='tight')
plt.show()
barras_verticales(dataframe_a_graficar)
Ver carpeta en GitHub
from pathlib import Path
import matplotlib.transforms as mtrans
from matplotlib.text import TextPath
from matplotlib.patches import PathPatch
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import matplotlib.font_manager as font_manager
# VARIABLES A CAMBIAR
dataframe_a_graficar = df
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def curly_at_fechas(x, y, width, height, ax=None, color="k"):
"""
Dibuja una llave '{' o '}' en cualquier lugar del gráfico.
Parámetros:
- x: Coordenada X (puede ser una fecha o un valor numérico).
- y: Coordenada Y.
- width: Ancho de la llave.
- height: Altura de la llave.
- ax: Eje de Matplotlib donde se dibujará la llave (opcional).
- color: Color del símbolo de la llave (por defecto es negro).
"""
if not ax:
ax = plt.gca()
# Si x es una fecha, convertirla a un valor numérico
if isinstance(x, pd.Timestamp):
x = date2num(x)
# Crear el símbolo de la llave con una fuente explícita
tp = TextPath((0, 0), "}", size=1, prop=dict(family="DejaVu Sans"))
# Escalar y trasladar la llave
trans = (
mtrans.Affine2D().scale(width, height) +
mtrans.Affine2D().translate(x, y) +
ax.transData
)
# Crear y añadir el PathPatch al eje con el color especificado
pp = PathPatch(tp, lw=0, fc=color, transform=trans)
ax.add_artist(pp)
def lineal(df, columna_fecha, columna_grafica, columna_linea, tipografia=None):
"""Genera una gráfica de área (area plot) para una columna específica de un DataFrame."""
# Verificar que las columnas especificadas existan en el DataFrame
if columna_fecha not in df.columns:
raise ValueError(f"La columna '{columna_fecha}' no existe en el DataFrame.")
if columna_grafica not in df.columns:
raise ValueError(f"La columna '{columna_grafica}' no existe en el DataFrame.")
if columna_linea not in df.columns:
raise ValueError(f"La columna '{columna_linea}' no existe en el DataFrame.")
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 18, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
# Configurar el tamaño de la figura en píxeles
ancho_px = 1480
alto_px = 520
dpi = 100 # Resolución en píxeles por pulgada
ancho_in = ancho_px / dpi
alto_in = alto_px / dpi
# Crear la figura con el tamaño especificado
fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
# Seleccionar colores específicos
color_area = '#d4dce9' # Primer color de la lista
color_linea = '#2f5597' # Segundo color de la lista
color_linea_punteada = '#c00000' # Color para la línea punteada
# Graficar el área
ax.fill_between(
df[columna_fecha],
df[columna_grafica],
color=color_area, # Color del área
alpha=1.,
)
# Graficar la línea sobre el área
ax.plot(
df[columna_fecha],
df[columna_grafica],
color=color_linea, # Color de la línea
linewidth=2
)
# Graficar la línea punteada
ax.plot(
df[columna_fecha],
df[columna_linea],
color=color_linea_punteada, # Color de la línea punteada
linestyle='--', # Estilo de línea punteada
linewidth=2
)
# Configurar etiquetas y título
ax.set_xlabel(columna_fecha, fontdict=font_config['eje_x'])
ax.set_ylabel("Valores", fontdict=font_config['eje_y'])
# Configurar las etiquetas del eje X
ax.tick_params(axis='x', labelsize=font_config['etiquetas_eje_x']['size'], labelcolor=font_config['etiquetas_eje_x']['color'])
ax.tick_params(axis='y', labelsize=font_config['etiquetas_eje_y']['size'], labelcolor=font_config['etiquetas_eje_y']['color'])
# Posicionar el eje X en y=0
ax.spines['bottom'].set_position(('data', 0))
# Rotar las etiquetas del eje X
plt.xticks(rotation=90)
# Desactivar o activar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(True)
# Asignar grosor a los ejes visibles
ax.spines['bottom'].set_linewidth(2) # Grosor del eje inferior
# Mantener las líneas del grid
ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
ax.grid(axis='x', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
# Convertir la fecha 2024 a un índice o posición en el eje X
x_pos = df["Fecha"].iloc[-1]
y_pos_gra = df[columna_grafica].iloc[-1]
y_pos_lin = df[columna_linea].iloc[-1]
if y_pos_gra > y_pos_lin:
y_pos_min = y_pos_lin
y_pos_max = y_pos_gra
else:
y_pos_min = y_pos_gra
y_pos_max = y_pos_lin
altura = y_pos_max - y_pos_min
# Calcular los factores proporcionales
x_min, x_max = ax.get_xlim() # Obtener los límites del eje X
y_min, y_max = ax.get_ylim() # Obtener los límites del eje Y
factor_ancho = 0.04 * (x_max - x_min) # Proporcional al rango del eje X
factor_alto = 0.05 * (y_max - y_min) # Proporcional al rango del eje Y
# Añadir el símbolo de la llave
curly_at_fechas(x_pos, y_pos_min+0.2*factor_alto, width=factor_ancho, height=altura+factor_alto, ax=ax, color="#af0b19")
# Calcular la diferencia entre y_pos_gra y y_pos_lin
diferencia = round(y_pos_gra - y_pos_lin)
# Añadir una cápsula con la diferencia en la parte derecha de la gráfica
color_capsula = "#af0b19" # Color de la cápsula
bbox_props = dict(boxstyle="round,pad=0.25,rounding_size=0.99", fc=color_capsula, ec="none", alpha=1.0)
x_pos_numeric = date2num(x_pos) # Convertir la fecha a un número
# Posicionar la cápsula en la parte derecha de la gráfica
ax.text(
x_pos_numeric + 1.8 * factor_ancho, # Usar el valor numérico de x_pos
y_pos_min + altura / 2.5, # Centrar verticalmente entre y_pos_gra y y_pos_lin
diferencia, # Texto de la cápsula
color="white", # Color del texto
fontsize=font_config['capsula_max']['size'], # Tamaño de la fuente
bbox=bbox_props, # Estilo de la cápsula
ha="center", # Alinear horizontalmente
va="center" # Alinear verticalmente
)
# Guardar la gráfica como archivo SVG
plt.savefig("linea.svg", format="svg", bbox_inches='tight', dpi=dpi)
plt.show()
# Ejemplo de uso
lineal(dataframe_a_graficar, columna_fecha="Fecha", columna_grafica="Columna1", columna_linea="Columna3")
Ver carpeta en GitHub
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from scipy.interpolate import make_interp_spline
# VARIABLES A CAMBIAR:
dataframe_a_graficar = data
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def snapshot(data):
"""
Genera un gráfico de área apilada con los datos proporcionados en un DataFrame.
Parámetros:
- data: DataFrame que contiene las columnas 'fechas', 'comisiones', 'fiscalias' y 'portal'.
"""
font_config = {
'family': 'Arial', # Cambiar a Arial
'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
'capsula_valor': {'size': 20, 'weight': 'medium', 'color': '#10302C'},
'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
'porcentaje': {'size': 20, 'weight': 'medium', 'color': 'white'},
'leyenda': {'size': 30, 'weight': 'medium', 'color': '#767676'} # Nueva categoría para la leyenda
}
# Extraer columnas del DataFrame
fechas = data['fechas']
comisiones = data['comisiones']
fiscalias = data['fiscalias']
portal = data['portal']
# Crear figura
fig, ax = plt.subplots(figsize=(16, 8))
n = len(fechas)
x = np.arange(n) + 0.5 # Desfase visual
fig_sub = plt.figure(figsize=(16, 8))
gs = fig.add_gridspec(1, 2, width_ratios=[3, 1], height_ratios=[1])
# ==============================================
# 2.1. PRIMER SUBPLOT - GRÁFICO DE ÁREAS
# ==============================================
ax1 = fig_sub.add_subplot(gs[0]) # Usa este, no crees otra figura
# Suavizar las curvas visualmente
x_suave = np.linspace(x.min(), x.max(), 300) # Más puntos para suavizar
comisiones_suave = make_interp_spline(x, comisiones)(x_suave)
fiscalias_suave = make_interp_spline(x, fiscalias)(x_suave)
portal_suave = make_interp_spline(x, portal)(x_suave)
x = np.arange(n) + 0.5
ax1.stackplot(x_suave, comisiones_suave, fiscalias_suave, portal_suave, labels=[
'Comisiones', 'Fiscalías y Procuradurías', 'Portal'],
colors=['#215F53', '#7570B3', '#C7EAE5'])
# Eje X
ax1.set_xticks(x)
ax1.set_xticklabels(fechas, rotation=90, fontsize=font_config['etiquetas_eje_x']['size'],
fontweight=font_config['etiquetas_eje_x']['weight'],
color=font_config['etiquetas_eje_x']['color'])
ax1.set_xlim(x[0] - 0.5, x[-1] + 0.5)
# Eje Y
ax1.set_yticks(np.linspace(0, 1, 5))
ax1.set_yticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=font_config['etiquetas_eje_y']['size'],
fontweight=font_config['etiquetas_eje_y']['weight'],
color=font_config['etiquetas_eje_y']['color'])
# Estética
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.spines['left'].set_visible(False)
ax1.spines['bottom'].set_linewidth(2)
ax1.grid(axis='y', linestyle='-', color='white', alpha=0.8, linewidth=1.25)
ax1.grid(axis='x', linestyle='-', color='white', alpha=0.3, linewidth=0.75)
# Etiquetas de totales
x_coords = [1.5, 6, 10.5]
y_coords = [0.15, 0.45, 0.85]
totales = [sum(comisiones), sum(fiscalias), sum(portal)]
colores_borde = ['#D9D9D9'] * 3
for x_c, y_c, total, color_borde in zip(x_coords, y_coords, totales, colores_borde):
ax1.text(x_c, y_c, f"{total:.2f}",
ha='center', va='center',
fontsize=font_config['capsula_valor']['size'],
color=font_config['capsula_valor']['color'],
bbox=dict(boxstyle="round,pad=0.6,rounding_size=1",
facecolor='white', edgecolor=color_borde, linewidth=1.8))
# ==============================================
# 2.2. SEGUNDO SUBPLOT - Snapshot
# ==============================================
ax2 = fig_sub.add_subplot(gs[1])
# Datos simulados (sept. 2006)
totales = np.array([sum(comisiones[:1]), sum(fiscalias[:1]), sum(portal[:1])])
totales_norm = totales / totales.sum()
colores = ['#215F53', '#7570B3', '#C7EAE5']
bar_width = 0.5
# Dibujar barra apilada
bottom = 0
for i, (valor, color) in enumerate(zip(totales_norm, colores)):
ax2.bar(0, valor, width=bar_width, bottom=bottom,
color=color, edgecolor='white', linewidth=0.5)
# Etiqueta de porcentaje en el centro
ax2.text(0, bottom + valor/2, f"{valor*100:.1f}%",
ha='center', va='center', color=font_config['porcentaje']['color'],
fontweight=font_config['porcentaje']['weight'],
fontsize=font_config['porcentaje']['size'])
bottom += valor
# Estética limpia
ax2.set_xlim(-0.5, 0.5)
ax2.set_ylim(0, 1)
ax2.set_xticks([])
ax2.set_yticks([])
for spine in ax2.spines.values():
spine.set_visible(False)
# Eliminar etiquetas de fecha del segundo subplot
ax2.tick_params(bottom=False, labelbottom=False)
# Evitar espacio excesivo
plt.subplots_adjust(wspace=0.05, hspace=0.05)
# Crear una instancia de FontProperties para la leyenda
leyenda_font = font_manager.FontProperties(
family=font_config['family'],
size=font_config['leyenda']['size'],
weight=font_config['leyenda']['weight']
)
# Leyenda común
fig_sub.legend(
['Comisiones', 'Fiscalías', 'Portal'],
loc='upper center',
ncol=3,
frameon=False,
fontsize=font_config['leyenda']['size'],
prop=leyenda_font, # Usar la instancia de FontProperties
bbox_to_anchor=(0.5, 1.05) # Ajustar la posición vertical (1.05 es un poco más arriba)
)
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("snapshot.svg", format="svg", bbox_inches='tight')
plt.show()
snapshot(dataframe_a_graficar)
Ver carpeta en GitHub
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
import squarify
# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad
# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'
# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.addfont(font_file)
# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))
def treemap(victimas_por_entidad):
if not isinstance(victimas_por_entidad, pd.Series):
raise ValueError("Los datos deben ser una Serie de pandas")
# Asegurar que todos los valores sean numéricos tipo float
victimas_por_entidad = pd.to_numeric(victimas_por_entidad, errors='coerce').fillna(0).astype(float)
# Configuración de fuentes y colores
font_config = {
'family': 'Arial',
'titulo': {'size': 18, 'weight': 'bold', 'color': '#10302C'},
'etiquetas': {'size': 10, 'weight': 'bold', 'color': 'white'},
'porcentaje': {'size': 9, 'weight': 'medium', 'color': '#A57F2C'}
}
try:
# Crear dataframe desde la serie
df = pd.DataFrame({
'ENTIDAD': victimas_por_entidad.index,
'Total': victimas_por_entidad.values
})
df.set_index('ENTIDAD', inplace=True)
# Ordenar y calcular porcentaje
df = df.sort_values(by='Total', ascending=False)
total_nacional = df['Total'].sum()
df['Porcentaje'] = (df['Total'] / total_nacional * 100).round(1)
except Exception as e:
print(f"Error: {e}")
return None
# Colores
max_valor = df['Total'].max()
colores = ['#10302C' if val == max_valor else '#4C6A67' for val in df['Total']]
# Configurar la figura
plt.rc('font', family=font_config['family'])
fig, ax = plt.subplots(figsize=(16, 10), dpi=300)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# rectangulos
sizes = df['Total'].tolist()
rectangles = squarify.normalize_sizes(sizes, 1, 1)
rectangles = squarify.squarify(rectangles, 0, 0, 1, 1)
for rect, (entidad, row), color in zip(rectangles, df.iterrows(), colores):
x, y, dx, dy = rect['x'], rect['y'], rect['dx'], rect['dy']
ax.add_patch(plt.Rectangle(
(x, y), dx, dy,
facecolor=color,
edgecolor='white',
linewidth=5
))
area = dx * dy
if area > 0.001:
palabras = entidad.split()
if len(palabras) > 2:
entidad_mod = ' '.join(palabras[:2]) + '\n' + ' '.join(palabras[2:])
else:
entidad_mod = entidad
etiqueta = f"{entidad_mod}\n{int(row['Total']):,}\n{row['Porcentaje']}%"
# Ajuste automático de tamaño de fuente según área
if area > 0.02:
fontsize = 10
elif area > 0.01:
fontsize = 8
else:
fontsize = 6
x_text = x + dx * 0.04
y_text = y + dy * 0.88
ax.text(
x_text,
y_text,
etiqueta,
ha='left',
va='top',
fontsize=fontsize,
fontweight=font_config['etiquetas']['weight'],
color=font_config['etiquetas']['color'],
zorder=10
)
ax.axis('off')
plt.tight_layout()
# Guardar la gráfica como archivo SVG
plt.savefig("treemap.svg", format="svg", bbox_inches='tight')
plt.show()
treemap(dataframe_a_graficar)
Ver carpeta en GitHub
# Crear una gráfica por estado
for estado in estados:
fig, ax = plt.subplots(figsize=(12, 6))
df_estado = df[df['estado'] == estado]
pivot_df = df_estado.pivot(index='fecha', columns='indicador', values='valor').fillna(0)
pivot_df[['Con datos', 'Sin datos']].plot(kind='bar', stacked=True, color=["#584290", "#b1adcf"], ax=ax, width=1)
ax.set_title(f'{estado} - Casos con y sin datos', fontsize=14, fontweight='bold')
ax.set_xlabel('Fecha')
ax.set_ylabel('Número de casos')
ax.set_xticks(range(0, len(pivot_df), 12))
ax.set_xticklabels([date.strftime('%Y') for date in pivot_df.index[::12]], rotation=45, fontsize=8)
ax.legend(title='')
ax.grid(False)
plt.tight_layout()
plt.show()
file_base = os.path.join(output_dir, f"{estado.replace(' ', '_')}_grafica_barras_tendencias")
fig.savefig(f"{file_base}.png", format="png", bbox_inches='tight', dpi=300)
fig.savefig(f"{file_base}.svg", format="svg", bbox_inches='tight', dpi=300)
plt.close(fig)
Ver carpeta en GitHub
# EXPORTAR: barras_apiladas
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess
def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
subprocess.run([
'scour', '-i', archivo_entrada, '-o', archivo_salida,
'--enable-viewboxing', '--enable-id-stripping',
'--shorten-ids', '--remove-descriptive-elements'
], check=True)
def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)
def formato_fechas(fechas):
fechas = pd.to_datetime(fechas)
# Si todos los años son diferentes, mostrar solo el año
if len(set(f.year for f in fechas)) == len(fechas):
return [str(f.year) for f in fechas]
else:
return [f.strftime("%d-%m-%Y") for f in fechas]
def barras_apiladas(
df_wide,
nombre=None,
font='Montserrat',
fontsize_barra=15,
fontsize_valor_total=20,
bar_height=0.5,
aumenta_ancho_fig=0,
aumenta_alto_fig=0,
aumenta_sep_leyenda=0.0,
valor_barra=False,
valor_total=True,
porcentaje_barra=False,
porcentaje_total=False,
porcentaje_total_inicio=False,
orientacion='horizontal',
ordenar_por='valor', # 'valor' o 'etiqueta'
orden='descendente', # 'ascendente' o 'descendente'
quitar_capsula=False,
area_min=0,
espacio_inicio=0, # Espacio para el porcentaje total al inicio de la barra
paleta_colores=None,
agregar_datos=None,
asignar_etiquetas=None,
grillas=True,
leyenda=None,
union_izquierda=0,
union_derecha=0,
sustituir_etiquetas=None,
separar_por_total=0.0,
y_limits=None,
nombre_eje_x=None,
nombre_eje_y=None,
resaltar_etiquetas=None
):
# Configuración de la fuente
font_config = {
'family': font,
'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
}
plt.rcParams['svg.fonttype'] = 'none'
font_dirs = [Path("../0_fonts")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
nombre_df = nombre or "barras_apiladas"
if paleta_colores:
colores_asignados = paleta_colores
else:
colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]
if isinstance(df_wide, pd.DataFrame):
df_wide = df_wide.copy()
categorias_x = df_wide.columns[0]
# Conversión automática de años enteros a fechas
if (
pd.api.types.is_integer_dtype(df_wide[categorias_x])
and df_wide[categorias_x].between(1900, 2100).all()
):
df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
df_wide = df_wide.set_index(categorias_x)
else:
raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")
# --- UNIÓN DE LAS PRIMERAS N BARRAS ---
etiqueta_union_izq = "Unión"
if union_izquierda > 1 and len(df_wide) >= union_izquierda:
union = df_wide.iloc[:union_izquierda].sum()
idxs = df_wide.index[:union_izquierda]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
pd.DataFrame([union], index=[etiqueta_union_izq]),
df_wide.iloc[union_izquierda:]
])
# --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
etiqueta_union_der = "Unión"
if union_derecha > 1 and len(df_wide) >= union_derecha:
union = df_wide.iloc[-union_derecha:].sum()
idxs = df_wide.index[-union_derecha:]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
df_wide.iloc[:-union_derecha],
pd.DataFrame([union], index=[etiqueta_union_der])
])
# --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
df_wide = df_wide.copy()
etiquetas_personalizadas = {}
if agregar_datos is not None:
filas_extra = []
for item in agregar_datos:
# Permite tuplas de 2 o 3 elementos
if len(item) == 3:
categoria, etiqueta, valor = item
etiquetas_personalizadas[categoria] = etiqueta
else:
categoria, valor = item
etiqueta = None
if isinstance(valor, (list, tuple, np.ndarray)):
fila = dict(zip(df_wide.columns, valor))
else:
fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
filas_extra.append(pd.Series(fila, name=categoria))
df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])
# --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
if agregar_datos is not None:
# Determina el tipo de índice para la barra espaciadora
if isinstance(df_wide.index[0], (int, np.integer, str)):
categoria_espaciadora = "__espaciador__"
else:
# Obtener la última fecha de agregar_datos y la primera fecha del resto
fechas_agregar = [d[0] for d in agregar_datos]
fecha_max_agregar = max(fechas_agregar)
fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
if fechas_resto:
fecha_min_resto = min(fechas_resto)
# Poner la fecha espaciadora justo entre ambas
categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
else:
# Si no hay resto, solo suma un día a la última de agregar_datos
categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
n_agregar = len(agregar_datos)
df_wide = pd.concat([
df_wide.iloc[:n_agregar], # barras de agregar_datos
pd.DataFrame([fila_espaciadora]), # espaciador
df_wide.iloc[n_agregar:] # el resto
])
etiquetas_finales = None
if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
etiquetas_finales = df_wide[asignar_etiquetas].copy()
df_wide = df_wide.drop(columns=[asignar_etiquetas])
# Calcular totales
suma_total = df_wide.sum(axis=1)
total_general = suma_total.sum()
# Definir x_max antes de dibujar las barras para evitar UnboundLocalError
x_max = suma_total.max() * 1.15
# ORDENAMIENTO
if ordenar_por == 'valor':
sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
elif ordenar_por == 'etiqueta':
# Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
indices = list(df_wide.index)
izq = []
der = []
centro = indices.copy()
if union_izquierda > 1:
izq = [etiqueta_union_izq]
centro = [i for i in centro if i != etiqueta_union_izq]
if union_derecha > 1:
der = [etiqueta_union_der]
centro = [i for i in centro if i != etiqueta_union_der]
tipos = set(type(idx) for idx in centro)
if len(tipos) > 1:
centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
else:
centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
sort_index = izq + centro_ordenado + der
else:
sort_index = df_wide.index # sin ordenar si valor inválido
df_wide = df_wide.loc[sort_index]
suma_total = suma_total.loc[sort_index]
entidades = df_wide.index.values
# --- FORMATEO DE ETIQUETAS ---
# Detectar índices de tipo fecha
indices_fechas = [
i for i, idx in enumerate(df_wide.index)
if isinstance(idx, (pd.Timestamp, np.datetime64))
]
# Formatear todas las fechas juntas si hay al menos una
if indices_fechas:
fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
else:
fechas_formateadas = []
entidades_formateadas = []
fecha_idx = 0
for i, idx in enumerate(df_wide.index):
if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
entidades_formateadas.append(idx)
elif isinstance(idx, (pd.Timestamp, np.datetime64)):
entidades_formateadas.append(fechas_formateadas[fecha_idx])
fecha_idx += 1
else:
entidades_formateadas.append(str(idx))
es_fecha = any(
isinstance(idx, (pd.Timestamp, np.datetime64))
for idx in df_wide.index
if not (
(union_izquierda > 1 and idx == etiqueta_union_izq) or
(union_derecha > 1 and idx == etiqueta_union_der)
)
)
# --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
if sustituir_etiquetas is not None:
if len(sustituir_etiquetas) != len(entidades_formateadas):
raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
entidades_formateadas = list(sustituir_etiquetas)
# Reemplaza etiquetas personalizadas si existen
if etiquetas_personalizadas:
for i, entidad in enumerate(df_wide.index):
if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
entidades_formateadas[i] = etiquetas_personalizadas[entidad]
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
# Detecta el valor exacto del espaciador
if es_fecha:
# El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
fechas_agregar = [d[0] for d in agregar_datos]
fechas_todas = list(df_wide.index)
fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
anio_espaciador = None
for idx in fechas_resto:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[idx] == 0).all():
anio_espaciador = idx
break
for i, entidad in enumerate(df_wide.index):
if entidad == anio_espaciador:
entidades_formateadas[i] = "..."
else:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
valores = [df_wide[col].values for col in df_wide.columns]
categorias = df_wide.columns
posiciones = np.arange(len(entidades))
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
# --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
if orientacion == 'vertical':
longitudes = []
for total_valor in suma_total:
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
longitudes.append(len(texto_capsula))
if longitudes:
moda_capsula_len = mode(longitudes)
else:
moda_capsula_len = 10
extra_width = moda_capsula_len * 0.1
base_width = max(12, len(entidades)*extra_width)
fig_width = base_width + aumenta_ancho_fig
fig_height = 8 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
else:
fig_width = 12 + aumenta_ancho_fig
fig_height = 15 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
# ---------------------------------------------------------------
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# Dibujar barras apiladas
for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
left = 0
for i, valor in enumerate(valores):
color = colores_asignados[i % len(colores_asignados)]
label = categorias[i] if pos == 0 else None
if orientacion == 'horizontal':
if valor[pos] > 0:
ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
else:
if valor[pos] > 0:
ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
if orientacion == 'vertical' and valor_barra and porcentaje_barra:
# Valor arriba, porcentaje abajo
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
va='bottom', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
va='top', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size']-2,
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
else:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
es_espaciador = entidad == "__espaciador__"
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
# Detecta si el índice es el espaciador
if es_fecha:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[entidad] == 0).all():
es_espaciador = True
else:
es_espaciador = entidad == "__espaciador__"
# Mostrar valor total o etiqueta personalizada solo si NO es espaciador
if valor_total and not es_espaciador:
# Si hay etiquetas_finales, usa esa etiqueta
etiqueta_personal = None
if etiquetas_finales is not None:
try:
etiqueta_personal = etiquetas_finales.iloc[pos]
except Exception:
etiqueta_personal = None
texto_a_mostrar = f"{int(total_valor):,}"
if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
texto_a_mostrar = str(etiqueta_personal)
if orientacion == 'horizontal':
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
if not quitar_capsula:
t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
else:
espacio = "\u00A0"
texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
if not quitar_capsula:
t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
# Mostrar porcentaje total al inicio de la barra si se solicita
# ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
if porcentaje_total_inicio:
porcentaje = round((total_valor / total_general) * 100, 1)
espacio = "\u00A0"
etiqueta_actual = entidades_formateadas[pos]
resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
if resaltar:
texto_capsula = f"{espacio*1}{etiqueta_actual} {porcentaje}%{espacio*1}"
bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
color_texto = "white"
if orientacion == 'horizontal':
ax.text(
-x_max*0.02, pos, texto_capsula,
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
ax.text(
pos, -x_max*0.02, texto_capsula,
ha='center', va='top',
rotation=90, # <-- Esto rota la cápsula y el texto
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
# Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
if orientacion == 'horizontal':
ax.text(
0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
else:
ax.text(
pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='top',
rotation=90, # <-- Esto rota el porcentaje solo
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
# Mostrar porcentaje total al lado derecho de la cápsula o del valor total
if porcentaje_total:
porcentaje = round((total_valor / total_general) * 100, 1)
if orientacion == 'horizontal':
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.15 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.18 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
total_valor + desplazamiento, pos, f"{porcentaje}%",
ha='left', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
else:
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.08 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.15 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
pos, total_valor + desplazamiento, f"{porcentaje}%",
ha='center', va='bottom',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
# Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
if porcentaje_total_inicio and resaltar_etiquetas is not None:
entidades_formateadas = [
"" if e in resaltar_etiquetas else e
for e in entidades_formateadas
]
# Configurar ejes y etiquetas
if orientacion == 'horizontal':
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_yticks(posiciones)
ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'])
ax.invert_yaxis()
ax.set_ylim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_xlim(y_limits)
else:
ax.set_xlim(0, 1.1 * x_max)
ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_xticklabels(),
fontsize=font_config['variable_x']['size'],
fontweight=font_config['variable_x']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_x']['color'])
if grillas:
ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='y', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
else:
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_xticks(posiciones)
ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
ax.set_xlim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_ylim(y_limits)
else:
ax.set_ylim(0, 1.1 * x_max)
ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_yticklabels(),
fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_y']['color'])
if grillas:
ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='x', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
# Ajustar rotación de etiquetas especiales según la orientación
if orientacion == 'vertical':
for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
if str(entidad) == "Previo":
label.set_rotation(90) # Cambia a vertical
label.set_ha('center')
elif str(entidad) == "...":
label.set_rotation(0)
label.set_ha('center')
else: # horizontal
for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
if str(entidad) == "...":
label.set_rotation(90)
label.set_va('center')
elif str(entidad) == "Previo":
label.set_rotation(0)
label.set_va('center')
if leyenda:
ncol_leyenda = len(df_wide.columns)
ax.legend(
title=leyenda if isinstance(leyenda, str) else None,
fontsize=font_config['leyenda']['size'],
title_fontsize=font_config['leyenda']['size'],
loc='upper center',
bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
frameon=False,
ncol=ncol_leyenda
)
# Quitar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
plt.tight_layout()
plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
nombre_svg = f"{nombre_df}.svg"
nombre_svg_limpio = f"{nombre_df}_scour.svg"
nombre_svg_svgo = f"{nombre_df}_svgo.svg"
try:
limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
except Exception as e:
print("Error al limpiar con Scour:", e)
try:
limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
except Exception as e:
print("Error al limpiar con SVGO:", e)
plt.show()
# EXPORTAR: barras_verticales_simples
barras_apiladas(df0,
nombre="barras_verticales_simples",
bar_height=0.9,
font='Montserrat',
fontsize_barra=20,
fontsize_valor_total=20,
ordenar_por='etiqueta',
orden='ascendente',
valor_barra=False,
porcentaje_barra=False,
porcentaje_total=True,
orientacion='vertical',
area_min=300
)
Ver carpeta en GitHub
# EXPORTAR: barras_apiladas
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess
def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
subprocess.run([
'scour', '-i', archivo_entrada, '-o', archivo_salida,
'--enable-viewboxing', '--enable-id-stripping',
'--shorten-ids', '--remove-descriptive-elements'
], check=True)
def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)
def formato_fechas(fechas):
fechas = pd.to_datetime(fechas)
# Si todos los años son diferentes, mostrar solo el año
if len(set(f.year for f in fechas)) == len(fechas):
return [str(f.year) for f in fechas]
else:
return [f.strftime("%d-%m-%Y") for f in fechas]
def barras_apiladas(
df_wide,
nombre=None,
font='Montserrat',
fontsize_barra=15,
fontsize_valor_total=20,
bar_height=0.5,
aumenta_ancho_fig=0,
aumenta_alto_fig=0,
aumenta_sep_leyenda=0.0,
valor_barra=False,
valor_total=True,
porcentaje_barra=False,
porcentaje_total=False,
porcentaje_total_inicio=False,
orientacion='horizontal',
ordenar_por='valor', # 'valor' o 'etiqueta'
orden='descendente', # 'ascendente' o 'descendente'
quitar_capsula=False,
area_min=0,
espacio_inicio=0, # Espacio para el porcentaje total al inicio de la barra
paleta_colores=None,
agregar_datos=None,
asignar_etiquetas=None,
grillas=True,
leyenda=None,
union_izquierda=0,
union_derecha=0,
sustituir_etiquetas=None,
separar_por_total=0.0,
y_limits=None,
nombre_eje_x=None,
nombre_eje_y=None,
resaltar_etiquetas=None
):
# Configuración de la fuente
font_config = {
'family': font,
'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
}
plt.rcParams['svg.fonttype'] = 'none'
font_dirs = [Path("../0_fonts")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
nombre_df = nombre or "barras_apiladas"
if paleta_colores:
colores_asignados = paleta_colores
else:
colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]
if isinstance(df_wide, pd.DataFrame):
df_wide = df_wide.copy()
categorias_x = df_wide.columns[0]
# Conversión automática de años enteros a fechas
if (
pd.api.types.is_integer_dtype(df_wide[categorias_x])
and df_wide[categorias_x].between(1900, 2100).all()
):
df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
df_wide = df_wide.set_index(categorias_x)
else:
raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")
# --- UNIÓN DE LAS PRIMERAS N BARRAS ---
etiqueta_union_izq = "Unión"
if union_izquierda > 1 and len(df_wide) >= union_izquierda:
union = df_wide.iloc[:union_izquierda].sum()
idxs = df_wide.index[:union_izquierda]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
pd.DataFrame([union], index=[etiqueta_union_izq]),
df_wide.iloc[union_izquierda:]
])
# --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
etiqueta_union_der = "Unión"
if union_derecha > 1 and len(df_wide) >= union_derecha:
union = df_wide.iloc[-union_derecha:].sum()
idxs = df_wide.index[-union_derecha:]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
df_wide.iloc[:-union_derecha],
pd.DataFrame([union], index=[etiqueta_union_der])
])
# --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
df_wide = df_wide.copy()
etiquetas_personalizadas = {}
if agregar_datos is not None:
filas_extra = []
for item in agregar_datos:
# Permite tuplas de 2 o 3 elementos
if len(item) == 3:
categoria, etiqueta, valor = item
etiquetas_personalizadas[categoria] = etiqueta
else:
categoria, valor = item
etiqueta = None
if isinstance(valor, (list, tuple, np.ndarray)):
fila = dict(zip(df_wide.columns, valor))
else:
fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
filas_extra.append(pd.Series(fila, name=categoria))
df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])
# --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
if agregar_datos is not None:
# Determina el tipo de índice para la barra espaciadora
if isinstance(df_wide.index[0], (int, np.integer, str)):
categoria_espaciadora = "__espaciador__"
else:
# Obtener la última fecha de agregar_datos y la primera fecha del resto
fechas_agregar = [d[0] for d in agregar_datos]
fecha_max_agregar = max(fechas_agregar)
fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
if fechas_resto:
fecha_min_resto = min(fechas_resto)
# Poner la fecha espaciadora justo entre ambas
categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
else:
# Si no hay resto, solo suma un día a la última de agregar_datos
categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
n_agregar = len(agregar_datos)
df_wide = pd.concat([
df_wide.iloc[:n_agregar], # barras de agregar_datos
pd.DataFrame([fila_espaciadora]), # espaciador
df_wide.iloc[n_agregar:] # el resto
])
etiquetas_finales = None
if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
etiquetas_finales = df_wide[asignar_etiquetas].copy()
df_wide = df_wide.drop(columns=[asignar_etiquetas])
# Calcular totales
suma_total = df_wide.sum(axis=1)
total_general = suma_total.sum()
# Definir x_max antes de dibujar las barras para evitar UnboundLocalError
x_max = suma_total.max() * 1.15
# ORDENAMIENTO
if ordenar_por == 'valor':
sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
elif ordenar_por == 'etiqueta':
# Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
indices = list(df_wide.index)
izq = []
der = []
centro = indices.copy()
if union_izquierda > 1:
izq = [etiqueta_union_izq]
centro = [i for i in centro if i != etiqueta_union_izq]
if union_derecha > 1:
der = [etiqueta_union_der]
centro = [i for i in centro if i != etiqueta_union_der]
tipos = set(type(idx) for idx in centro)
if len(tipos) > 1:
centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
else:
centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
sort_index = izq + centro_ordenado + der
else:
sort_index = df_wide.index # sin ordenar si valor inválido
df_wide = df_wide.loc[sort_index]
suma_total = suma_total.loc[sort_index]
entidades = df_wide.index.values
# --- FORMATEO DE ETIQUETAS ---
# Detectar índices de tipo fecha
indices_fechas = [
i for i, idx in enumerate(df_wide.index)
if isinstance(idx, (pd.Timestamp, np.datetime64))
]
# Formatear todas las fechas juntas si hay al menos una
if indices_fechas:
fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
else:
fechas_formateadas = []
entidades_formateadas = []
fecha_idx = 0
for i, idx in enumerate(df_wide.index):
if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
entidades_formateadas.append(idx)
elif isinstance(idx, (pd.Timestamp, np.datetime64)):
entidades_formateadas.append(fechas_formateadas[fecha_idx])
fecha_idx += 1
else:
entidades_formateadas.append(str(idx))
es_fecha = any(
isinstance(idx, (pd.Timestamp, np.datetime64))
for idx in df_wide.index
if not (
(union_izquierda > 1 and idx == etiqueta_union_izq) or
(union_derecha > 1 and idx == etiqueta_union_der)
)
)
# --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
if sustituir_etiquetas is not None:
if len(sustituir_etiquetas) != len(entidades_formateadas):
raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
entidades_formateadas = list(sustituir_etiquetas)
# Reemplaza etiquetas personalizadas si existen
if etiquetas_personalizadas:
for i, entidad in enumerate(df_wide.index):
if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
entidades_formateadas[i] = etiquetas_personalizadas[entidad]
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
# Detecta el valor exacto del espaciador
if es_fecha:
# El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
fechas_agregar = [d[0] for d in agregar_datos]
fechas_todas = list(df_wide.index)
fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
anio_espaciador = None
for idx in fechas_resto:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[idx] == 0).all():
anio_espaciador = idx
break
for i, entidad in enumerate(df_wide.index):
if entidad == anio_espaciador:
entidades_formateadas[i] = "..."
else:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
valores = [df_wide[col].values for col in df_wide.columns]
categorias = df_wide.columns
posiciones = np.arange(len(entidades))
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
# --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
if orientacion == 'vertical':
longitudes = []
for total_valor in suma_total:
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
longitudes.append(len(texto_capsula))
if longitudes:
moda_capsula_len = mode(longitudes)
else:
moda_capsula_len = 10
extra_width = moda_capsula_len * 0.1
base_width = max(12, len(entidades)*extra_width)
fig_width = base_width + aumenta_ancho_fig
fig_height = 8 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
else:
fig_width = 12 + aumenta_ancho_fig
fig_height = 15 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
# ---------------------------------------------------------------
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# Dibujar barras apiladas
for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
left = 0
for i, valor in enumerate(valores):
color = colores_asignados[i % len(colores_asignados)]
label = categorias[i] if pos == 0 else None
if orientacion == 'horizontal':
if valor[pos] > 0:
ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
else:
if valor[pos] > 0:
ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
if orientacion == 'vertical' and valor_barra and porcentaje_barra:
# Valor arriba, porcentaje abajo
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
va='bottom', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
va='top', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size']-2,
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
else:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
es_espaciador = entidad == "__espaciador__"
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
# Detecta si el índice es el espaciador
if es_fecha:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[entidad] == 0).all():
es_espaciador = True
else:
es_espaciador = entidad == "__espaciador__"
# Mostrar valor total o etiqueta personalizada solo si NO es espaciador
if valor_total and not es_espaciador:
# Si hay etiquetas_finales, usa esa etiqueta
etiqueta_personal = None
if etiquetas_finales is not None:
try:
etiqueta_personal = etiquetas_finales.iloc[pos]
except Exception:
etiqueta_personal = None
texto_a_mostrar = f"{int(total_valor):,}"
if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
texto_a_mostrar = str(etiqueta_personal)
if orientacion == 'horizontal':
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
if not quitar_capsula:
t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
else:
espacio = "\u00A0"
texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
if not quitar_capsula:
t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
# Mostrar porcentaje total al inicio de la barra si se solicita
# ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
if porcentaje_total_inicio:
porcentaje = round((total_valor / total_general) * 100, 1)
espacio = "\u00A0"
etiqueta_actual = entidades_formateadas[pos]
resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
if resaltar:
texto_capsula = f"{espacio*1}{etiqueta_actual} {porcentaje}%{espacio*1}"
bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
color_texto = "white"
if orientacion == 'horizontal':
ax.text(
-x_max*0.02, pos, texto_capsula,
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
ax.text(
pos, -x_max*0.02, texto_capsula,
ha='center', va='top',
rotation=90, # <-- Esto rota la cápsula y el texto
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
# Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
if orientacion == 'horizontal':
ax.text(
0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
else:
ax.text(
pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='top',
rotation=90, # <-- Esto rota el porcentaje solo
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
# Mostrar porcentaje total al lado derecho de la cápsula o del valor total
if porcentaje_total:
porcentaje = round((total_valor / total_general) * 100, 1)
if orientacion == 'horizontal':
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.15 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.18 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
total_valor + desplazamiento, pos, f"{porcentaje}%",
ha='left', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
else:
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.08 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.15 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
pos, total_valor + desplazamiento, f"{porcentaje}%",
ha='center', va='bottom',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
# Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
if porcentaje_total_inicio and resaltar_etiquetas is not None:
entidades_formateadas = [
"" if e in resaltar_etiquetas else e
for e in entidades_formateadas
]
# Configurar ejes y etiquetas
if orientacion == 'horizontal':
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_yticks(posiciones)
ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'])
ax.invert_yaxis()
ax.set_ylim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_xlim(y_limits)
else:
ax.set_xlim(0, 1.1 * x_max)
ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_xticklabels(),
fontsize=font_config['variable_x']['size'],
fontweight=font_config['variable_x']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_x']['color'])
if grillas:
ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='y', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
else:
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_xticks(posiciones)
ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
ax.set_xlim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_ylim(y_limits)
else:
ax.set_ylim(0, 1.1 * x_max)
ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_yticklabels(),
fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_y']['color'])
if grillas:
ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='x', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
# Ajustar rotación de etiquetas especiales según la orientación
if orientacion == 'vertical':
for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
if str(entidad) == "Previo":
label.set_rotation(90) # Cambia a vertical
label.set_ha('center')
elif str(entidad) == "...":
label.set_rotation(0)
label.set_ha('center')
else: # horizontal
for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
if str(entidad) == "...":
label.set_rotation(90)
label.set_va('center')
elif str(entidad) == "Previo":
label.set_rotation(0)
label.set_va('center')
if leyenda:
ncol_leyenda = len(df_wide.columns)
ax.legend(
title=leyenda if isinstance(leyenda, str) else None,
fontsize=font_config['leyenda']['size'],
title_fontsize=font_config['leyenda']['size'],
loc='upper center',
bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
frameon=False,
ncol=ncol_leyenda
)
# Quitar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
plt.tight_layout()
plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
nombre_svg = f"{nombre_df}.svg"
nombre_svg_limpio = f"{nombre_df}_scour.svg"
nombre_svg_svgo = f"{nombre_df}_svgo.svg"
try:
limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
except Exception as e:
print("Error al limpiar con Scour:", e)
try:
limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
except Exception as e:
print("Error al limpiar con SVGO:", e)
plt.show()
Ver carpeta en GitHub
# EXPORTAR: barras_apiladas
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess
def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
subprocess.run([
'scour', '-i', archivo_entrada, '-o', archivo_salida,
'--enable-viewboxing', '--enable-id-stripping',
'--shorten-ids', '--remove-descriptive-elements'
], check=True)
def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)
def formato_fechas(fechas):
fechas = pd.to_datetime(fechas)
# Si todos los años son diferentes, mostrar solo el año
if len(set(f.year for f in fechas)) == len(fechas):
return [str(f.year) for f in fechas]
else:
return [f.strftime("%d-%m-%Y") for f in fechas]
def barras_apiladas(
df_wide,
nombre=None,
font='Montserrat',
fontsize_barra=15,
fontsize_valor_total=20,
bar_height=0.5,
aumenta_ancho_fig=0,
aumenta_alto_fig=0,
aumenta_sep_leyenda=0.0,
valor_barra=False,
valor_total=True,
porcentaje_barra=False,
porcentaje_total=False,
porcentaje_total_inicio=False,
orientacion='horizontal',
ordenar_por='valor', # 'valor' o 'etiqueta'
orden='descendente', # 'ascendente' o 'descendente'
quitar_capsula=False,
area_min=0,
espacio_inicio=0, # Espacio para el porcentaje total al inicio de la barra
paleta_colores=None,
agregar_datos=None,
asignar_etiquetas=None,
grillas=True,
leyenda=None,
union_izquierda=0,
union_derecha=0,
sustituir_etiquetas=None,
separar_por_total=0.0,
y_limits=None,
nombre_eje_x=None,
nombre_eje_y=None,
resaltar_etiquetas=None
):
# Configuración de la fuente
font_config = {
'family': font,
'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
}
plt.rcParams['svg.fonttype'] = 'none'
font_dirs = [Path("../0_fonts")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
nombre_df = nombre or "barras_apiladas"
if paleta_colores:
colores_asignados = paleta_colores
else:
colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]
if isinstance(df_wide, pd.DataFrame):
df_wide = df_wide.copy()
categorias_x = df_wide.columns[0]
# Conversión automática de años enteros a fechas
if (
pd.api.types.is_integer_dtype(df_wide[categorias_x])
and df_wide[categorias_x].between(1900, 2100).all()
):
df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
df_wide = df_wide.set_index(categorias_x)
else:
raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")
# --- UNIÓN DE LAS PRIMERAS N BARRAS ---
etiqueta_union_izq = "Unión"
if union_izquierda > 1 and len(df_wide) >= union_izquierda:
union = df_wide.iloc[:union_izquierda].sum()
idxs = df_wide.index[:union_izquierda]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
pd.DataFrame([union], index=[etiqueta_union_izq]),
df_wide.iloc[union_izquierda:]
])
# --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
etiqueta_union_der = "Unión"
if union_derecha > 1 and len(df_wide) >= union_derecha:
union = df_wide.iloc[-union_derecha:].sum()
idxs = df_wide.index[-union_derecha:]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
df_wide.iloc[:-union_derecha],
pd.DataFrame([union], index=[etiqueta_union_der])
])
# --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
df_wide = df_wide.copy()
etiquetas_personalizadas = {}
if agregar_datos is not None:
filas_extra = []
for item in agregar_datos:
# Permite tuplas de 2 o 3 elementos
if len(item) == 3:
categoria, etiqueta, valor = item
etiquetas_personalizadas[categoria] = etiqueta
else:
categoria, valor = item
etiqueta = None
if isinstance(valor, (list, tuple, np.ndarray)):
fila = dict(zip(df_wide.columns, valor))
else:
fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
filas_extra.append(pd.Series(fila, name=categoria))
df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])
# --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
if agregar_datos is not None:
# Determina el tipo de índice para la barra espaciadora
if isinstance(df_wide.index[0], (int, np.integer, str)):
categoria_espaciadora = "__espaciador__"
else:
# Obtener la última fecha de agregar_datos y la primera fecha del resto
fechas_agregar = [d[0] for d in agregar_datos]
fecha_max_agregar = max(fechas_agregar)
fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
if fechas_resto:
fecha_min_resto = min(fechas_resto)
# Poner la fecha espaciadora justo entre ambas
categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
else:
# Si no hay resto, solo suma un día a la última de agregar_datos
categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
n_agregar = len(agregar_datos)
df_wide = pd.concat([
df_wide.iloc[:n_agregar], # barras de agregar_datos
pd.DataFrame([fila_espaciadora]), # espaciador
df_wide.iloc[n_agregar:] # el resto
])
etiquetas_finales = None
if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
etiquetas_finales = df_wide[asignar_etiquetas].copy()
df_wide = df_wide.drop(columns=[asignar_etiquetas])
# Calcular totales
suma_total = df_wide.sum(axis=1)
total_general = suma_total.sum()
# Definir x_max antes de dibujar las barras para evitar UnboundLocalError
x_max = suma_total.max() * 1.15
# ORDENAMIENTO
if ordenar_por == 'valor':
sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
elif ordenar_por == 'etiqueta':
# Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
indices = list(df_wide.index)
izq = []
der = []
centro = indices.copy()
if union_izquierda > 1:
izq = [etiqueta_union_izq]
centro = [i for i in centro if i != etiqueta_union_izq]
if union_derecha > 1:
der = [etiqueta_union_der]
centro = [i for i in centro if i != etiqueta_union_der]
tipos = set(type(idx) for idx in centro)
if len(tipos) > 1:
centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
else:
centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
sort_index = izq + centro_ordenado + der
else:
sort_index = df_wide.index # sin ordenar si valor inválido
df_wide = df_wide.loc[sort_index]
suma_total = suma_total.loc[sort_index]
entidades = df_wide.index.values
# --- FORMATEO DE ETIQUETAS ---
# Detectar índices de tipo fecha
indices_fechas = [
i for i, idx in enumerate(df_wide.index)
if isinstance(idx, (pd.Timestamp, np.datetime64))
]
# Formatear todas las fechas juntas si hay al menos una
if indices_fechas:
fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
else:
fechas_formateadas = []
entidades_formateadas = []
fecha_idx = 0
for i, idx in enumerate(df_wide.index):
if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
entidades_formateadas.append(idx)
elif isinstance(idx, (pd.Timestamp, np.datetime64)):
entidades_formateadas.append(fechas_formateadas[fecha_idx])
fecha_idx += 1
else:
entidades_formateadas.append(str(idx))
es_fecha = any(
isinstance(idx, (pd.Timestamp, np.datetime64))
for idx in df_wide.index
if not (
(union_izquierda > 1 and idx == etiqueta_union_izq) or
(union_derecha > 1 and idx == etiqueta_union_der)
)
)
# --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
if sustituir_etiquetas is not None:
if len(sustituir_etiquetas) != len(entidades_formateadas):
raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
entidades_formateadas = list(sustituir_etiquetas)
# Reemplaza etiquetas personalizadas si existen
if etiquetas_personalizadas:
for i, entidad in enumerate(df_wide.index):
if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
entidades_formateadas[i] = etiquetas_personalizadas[entidad]
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
# Detecta el valor exacto del espaciador
if es_fecha:
# El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
fechas_agregar = [d[0] for d in agregar_datos]
fechas_todas = list(df_wide.index)
fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
anio_espaciador = None
for idx in fechas_resto:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[idx] == 0).all():
anio_espaciador = idx
break
for i, entidad in enumerate(df_wide.index):
if entidad == anio_espaciador:
entidades_formateadas[i] = "..."
else:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
valores = [df_wide[col].values for col in df_wide.columns]
categorias = df_wide.columns
posiciones = np.arange(len(entidades))
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
# --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
if orientacion == 'vertical':
longitudes = []
for total_valor in suma_total:
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
longitudes.append(len(texto_capsula))
if longitudes:
moda_capsula_len = mode(longitudes)
else:
moda_capsula_len = 10
extra_width = moda_capsula_len * 0.1
base_width = max(12, len(entidades)*extra_width)
fig_width = base_width + aumenta_ancho_fig
fig_height = 8 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
else:
fig_width = 12 + aumenta_ancho_fig
fig_height = 15 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
# ---------------------------------------------------------------
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# Dibujar barras apiladas
for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
left = 0
for i, valor in enumerate(valores):
color = colores_asignados[i % len(colores_asignados)]
label = categorias[i] if pos == 0 else None
if orientacion == 'horizontal':
if valor[pos] > 0:
ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
else:
if valor[pos] > 0:
ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
if orientacion == 'vertical' and valor_barra and porcentaje_barra:
# Valor arriba, porcentaje abajo
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
va='bottom', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
va='top', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size']-2,
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
else:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
es_espaciador = entidad == "__espaciador__"
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
# Detecta si el índice es el espaciador
if es_fecha:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[entidad] == 0).all():
es_espaciador = True
else:
es_espaciador = entidad == "__espaciador__"
# Mostrar valor total o etiqueta personalizada solo si NO es espaciador
if valor_total and not es_espaciador:
# Si hay etiquetas_finales, usa esa etiqueta
etiqueta_personal = None
if etiquetas_finales is not None:
try:
etiqueta_personal = etiquetas_finales.iloc[pos]
except Exception:
etiqueta_personal = None
texto_a_mostrar = f"{int(total_valor):,}"
if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
texto_a_mostrar = str(etiqueta_personal)
if orientacion == 'horizontal':
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
if not quitar_capsula:
t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
else:
espacio = "\u00A0"
texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
if not quitar_capsula:
t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
# Mostrar porcentaje total al inicio de la barra si se solicita
# ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
if porcentaje_total_inicio:
porcentaje = round((total_valor / total_general) * 100, 1)
espacio = "\u00A0"
etiqueta_actual = entidades_formateadas[pos]
resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
if resaltar:
texto_capsula = f"{espacio*1}{etiqueta_actual} {porcentaje}%{espacio*1}"
bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
color_texto = "white"
if orientacion == 'horizontal':
ax.text(
-x_max*0.02, pos, texto_capsula,
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
ax.text(
pos, -x_max*0.02, texto_capsula,
ha='center', va='top',
rotation=90, # <-- Esto rota la cápsula y el texto
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
# Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
if orientacion == 'horizontal':
ax.text(
0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
else:
ax.text(
pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='top',
rotation=90, # <-- Esto rota el porcentaje solo
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
# Mostrar porcentaje total al lado derecho de la cápsula o del valor total
if porcentaje_total:
porcentaje = round((total_valor / total_general) * 100, 1)
if orientacion == 'horizontal':
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.15 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.18 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
total_valor + desplazamiento, pos, f"{porcentaje}%",
ha='left', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
else:
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.08 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.15 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
pos, total_valor + desplazamiento, f"{porcentaje}%",
ha='center', va='bottom',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
# Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
if porcentaje_total_inicio and resaltar_etiquetas is not None:
entidades_formateadas = [
"" if e in resaltar_etiquetas else e
for e in entidades_formateadas
]
# Configurar ejes y etiquetas
if orientacion == 'horizontal':
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_yticks(posiciones)
ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'])
ax.invert_yaxis()
ax.set_ylim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_xlim(y_limits)
else:
ax.set_xlim(0, 1.1 * x_max)
ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_xticklabels(),
fontsize=font_config['variable_x']['size'],
fontweight=font_config['variable_x']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_x']['color'])
if grillas:
ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='y', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
else:
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_xticks(posiciones)
ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
ax.set_xlim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_ylim(y_limits)
else:
ax.set_ylim(0, 1.1 * x_max)
ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_yticklabels(),
fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_y']['color'])
if grillas:
ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='x', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
# Ajustar rotación de etiquetas especiales según la orientación
if orientacion == 'vertical':
for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
if str(entidad) == "Previo":
label.set_rotation(90) # Cambia a vertical
label.set_ha('center')
elif str(entidad) == "...":
label.set_rotation(0)
label.set_ha('center')
else: # horizontal
for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
if str(entidad) == "...":
label.set_rotation(90)
label.set_va('center')
elif str(entidad) == "Previo":
label.set_rotation(0)
label.set_va('center')
if leyenda:
ncol_leyenda = len(df_wide.columns)
ax.legend(
title=leyenda if isinstance(leyenda, str) else None,
fontsize=font_config['leyenda']['size'],
title_fontsize=font_config['leyenda']['size'],
loc='upper center',
bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
frameon=False,
ncol=ncol_leyenda
)
# Quitar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
plt.tight_layout()
plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
nombre_svg = f"{nombre_df}.svg"
nombre_svg_limpio = f"{nombre_df}_scour.svg"
nombre_svg_svgo = f"{nombre_df}_svgo.svg"
try:
limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
except Exception as e:
print("Error al limpiar con Scour:", e)
try:
limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
except Exception as e:
print("Error al limpiar con SVGO:", e)
plt.show()
# EXPORTAR: barra_previa
barras_apiladas(df1f,
nombre="barra_previa",
bar_height=0.9,
font='Montserrat',
fontsize_valor_total=30,
ordenar_por='etiqueta',
orden='ascendente',
valor_barra=False,
porcentaje_barra=False,
porcentaje_total=False,
orientacion='vertical',
quitar_capsula=True,
agregar_datos=[(pd.Timestamp(2005, 1, 1), "Previo", (192, 313, "38%"))],
asignar_etiquetas='porcentaje',
grillas=False,
paleta_colores=["#10302C", "#E6E6E6"]
)
Ver carpeta en GitHub
# EXPORTAR: barras_apiladas
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess
def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
subprocess.run([
'scour', '-i', archivo_entrada, '-o', archivo_salida,
'--enable-viewboxing', '--enable-id-stripping',
'--shorten-ids', '--remove-descriptive-elements'
], check=True)
def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)
def formato_fechas(fechas):
fechas = pd.to_datetime(fechas)
# Si todos los años son diferentes, mostrar solo el año
if len(set(f.year for f in fechas)) == len(fechas):
return [str(f.year) for f in fechas]
else:
return [f.strftime("%d-%m-%Y") for f in fechas]
def barras_apiladas(
df_wide,
nombre=None,
font='Montserrat',
fontsize_barra=15,
fontsize_valor_total=20,
bar_height=0.5,
aumenta_ancho_fig=0,
aumenta_alto_fig=0,
aumenta_sep_leyenda=0.0,
valor_barra=False,
valor_total=True,
porcentaje_barra=False,
porcentaje_total=False,
porcentaje_total_inicio=False,
orientacion='horizontal',
ordenar_por='valor', # 'valor' o 'etiqueta'
orden='descendente', # 'ascendente' o 'descendente'
quitar_capsula=False,
area_min=0,
espacio_inicio=0, # Espacio para el porcentaje total al inicio de la barra
paleta_colores=None,
agregar_datos=None,
asignar_etiquetas=None,
grillas=True,
leyenda=None,
union_izquierda=0,
union_derecha=0,
sustituir_etiquetas=None,
separar_por_total=0.0,
y_limits=None,
nombre_eje_x=None,
nombre_eje_y=None,
resaltar_etiquetas=None
):
# Configuración de la fuente
font_config = {
'family': font,
'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
}
plt.rcParams['svg.fonttype'] = 'none'
font_dirs = [Path("../0_fonts")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
for font_file in font_files:
font_manager.fontManager.addfont(font_file)
nombre_df = nombre or "barras_apiladas"
if paleta_colores:
colores_asignados = paleta_colores
else:
colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]
if isinstance(df_wide, pd.DataFrame):
df_wide = df_wide.copy()
categorias_x = df_wide.columns[0]
# Conversión automática de años enteros a fechas
if (
pd.api.types.is_integer_dtype(df_wide[categorias_x])
and df_wide[categorias_x].between(1900, 2100).all()
):
df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
df_wide = df_wide.set_index(categorias_x)
else:
raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")
# --- UNIÓN DE LAS PRIMERAS N BARRAS ---
etiqueta_union_izq = "Unión"
if union_izquierda > 1 and len(df_wide) >= union_izquierda:
union = df_wide.iloc[:union_izquierda].sum()
idxs = df_wide.index[:union_izquierda]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
pd.DataFrame([union], index=[etiqueta_union_izq]),
df_wide.iloc[union_izquierda:]
])
# --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
etiqueta_union_der = "Unión"
if union_derecha > 1 and len(df_wide) >= union_derecha:
union = df_wide.iloc[-union_derecha:].sum()
idxs = df_wide.index[-union_derecha:]
if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
elif isinstance(idxs[0], (int, np.integer, float)):
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
else:
etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
df_wide = pd.concat([
df_wide.iloc[:-union_derecha],
pd.DataFrame([union], index=[etiqueta_union_der])
])
# --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
df_wide = df_wide.copy()
etiquetas_personalizadas = {}
if agregar_datos is not None:
filas_extra = []
for item in agregar_datos:
# Permite tuplas de 2 o 3 elementos
if len(item) == 3:
categoria, etiqueta, valor = item
etiquetas_personalizadas[categoria] = etiqueta
else:
categoria, valor = item
etiqueta = None
if isinstance(valor, (list, tuple, np.ndarray)):
fila = dict(zip(df_wide.columns, valor))
else:
fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
filas_extra.append(pd.Series(fila, name=categoria))
df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])
# --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
if agregar_datos is not None:
# Determina el tipo de índice para la barra espaciadora
if isinstance(df_wide.index[0], (int, np.integer, str)):
categoria_espaciadora = "__espaciador__"
else:
# Obtener la última fecha de agregar_datos y la primera fecha del resto
fechas_agregar = [d[0] for d in agregar_datos]
fecha_max_agregar = max(fechas_agregar)
fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
if fechas_resto:
fecha_min_resto = min(fechas_resto)
# Poner la fecha espaciadora justo entre ambas
categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
else:
# Si no hay resto, solo suma un día a la última de agregar_datos
categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
n_agregar = len(agregar_datos)
df_wide = pd.concat([
df_wide.iloc[:n_agregar], # barras de agregar_datos
pd.DataFrame([fila_espaciadora]), # espaciador
df_wide.iloc[n_agregar:] # el resto
])
etiquetas_finales = None
if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
etiquetas_finales = df_wide[asignar_etiquetas].copy()
df_wide = df_wide.drop(columns=[asignar_etiquetas])
# Calcular totales
suma_total = df_wide.sum(axis=1)
total_general = suma_total.sum()
# Definir x_max antes de dibujar las barras para evitar UnboundLocalError
x_max = suma_total.max() * 1.15
# ORDENAMIENTO
if ordenar_por == 'valor':
sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
elif ordenar_por == 'etiqueta':
# Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
indices = list(df_wide.index)
izq = []
der = []
centro = indices.copy()
if union_izquierda > 1:
izq = [etiqueta_union_izq]
centro = [i for i in centro if i != etiqueta_union_izq]
if union_derecha > 1:
der = [etiqueta_union_der]
centro = [i for i in centro if i != etiqueta_union_der]
tipos = set(type(idx) for idx in centro)
if len(tipos) > 1:
centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
else:
centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
sort_index = izq + centro_ordenado + der
else:
sort_index = df_wide.index # sin ordenar si valor inválido
df_wide = df_wide.loc[sort_index]
suma_total = suma_total.loc[sort_index]
entidades = df_wide.index.values
# --- FORMATEO DE ETIQUETAS ---
# Detectar índices de tipo fecha
indices_fechas = [
i for i, idx in enumerate(df_wide.index)
if isinstance(idx, (pd.Timestamp, np.datetime64))
]
# Formatear todas las fechas juntas si hay al menos una
if indices_fechas:
fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
else:
fechas_formateadas = []
entidades_formateadas = []
fecha_idx = 0
for i, idx in enumerate(df_wide.index):
if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
entidades_formateadas.append(idx)
elif isinstance(idx, (pd.Timestamp, np.datetime64)):
entidades_formateadas.append(fechas_formateadas[fecha_idx])
fecha_idx += 1
else:
entidades_formateadas.append(str(idx))
es_fecha = any(
isinstance(idx, (pd.Timestamp, np.datetime64))
for idx in df_wide.index
if not (
(union_izquierda > 1 and idx == etiqueta_union_izq) or
(union_derecha > 1 and idx == etiqueta_union_der)
)
)
# --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
if sustituir_etiquetas is not None:
if len(sustituir_etiquetas) != len(entidades_formateadas):
raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
entidades_formateadas = list(sustituir_etiquetas)
# Reemplaza etiquetas personalizadas si existen
if etiquetas_personalizadas:
for i, entidad in enumerate(df_wide.index):
if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
entidades_formateadas[i] = etiquetas_personalizadas[entidad]
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
# Detecta el valor exacto del espaciador
if es_fecha:
# El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
fechas_agregar = [d[0] for d in agregar_datos]
fechas_todas = list(df_wide.index)
fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
anio_espaciador = None
for idx in fechas_resto:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[idx] == 0).all():
anio_espaciador = idx
break
for i, entidad in enumerate(df_wide.index):
if entidad == anio_espaciador:
entidades_formateadas[i] = "..."
else:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
valores = [df_wide[col].values for col in df_wide.columns]
categorias = df_wide.columns
posiciones = np.arange(len(entidades))
# Si hay espaciador, pon etiqueta "..."
if agregar_datos is not None:
for i, entidad in enumerate(entidades_formateadas):
if entidad == "__espaciador__":
entidades_formateadas[i] = "..."
# --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
if orientacion == 'vertical':
longitudes = []
for total_valor in suma_total:
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
longitudes.append(len(texto_capsula))
if longitudes:
moda_capsula_len = mode(longitudes)
else:
moda_capsula_len = 10
extra_width = moda_capsula_len * 0.1
base_width = max(12, len(entidades)*extra_width)
fig_width = base_width + aumenta_ancho_fig
fig_height = 8 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
else:
fig_width = 12 + aumenta_ancho_fig
fig_height = 15 + aumenta_alto_fig
fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
# ---------------------------------------------------------------
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# Dibujar barras apiladas
for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
left = 0
for i, valor in enumerate(valores):
color = colores_asignados[i % len(colores_asignados)]
label = categorias[i] if pos == 0 else None
if orientacion == 'horizontal':
if valor[pos] > 0:
ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
else:
if valor[pos] > 0:
ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
area_barra = valor[pos] * bar_height
if (porcentaje_barra or valor_barra) and area_barra >= area_min:
if orientacion == 'vertical' and valor_barra and porcentaje_barra:
# Valor arriba, porcentaje abajo
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
va='bottom', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
va='top', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size']-2,
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
else:
texto = f"{valor[pos]:,.0f}" if valor_barra else ""
if porcentaje_barra:
porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
fontsize=font_config['valor_porcentaje_barra']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_porcentaje_barra']['weight'],
color=font_config['valor_porcentaje_barra']['color'])
left += valor[pos]
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
es_espaciador = entidad == "__espaciador__"
# Determina si es la barra espaciadora
es_espaciador = False
if agregar_datos is not None:
# Detecta si el índice es el espaciador
if es_fecha:
# El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
if (df_wide.loc[entidad] == 0).all():
es_espaciador = True
else:
es_espaciador = entidad == "__espaciador__"
# Mostrar valor total o etiqueta personalizada solo si NO es espaciador
if valor_total and not es_espaciador:
# Si hay etiquetas_finales, usa esa etiqueta
etiqueta_personal = None
if etiquetas_finales is not None:
try:
etiqueta_personal = etiquetas_finales.iloc[pos]
except Exception:
etiqueta_personal = None
texto_a_mostrar = f"{int(total_valor):,}"
if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
texto_a_mostrar = str(etiqueta_personal)
if orientacion == 'horizontal':
espacio = "\u00A0"
texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
if not quitar_capsula:
t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
ha='left', va='center',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
else:
espacio = "\u00A0"
texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
if not quitar_capsula:
t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
fig.canvas.draw()
else:
ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
ha='center', va='bottom',
fontsize=font_config['valor_capsula']['size'],
fontfamily=font_config['family'],
fontweight=font_config['valor_capsula']['weight'],
color=font_config['valor_capsula']['color'])
# Mostrar porcentaje total al inicio de la barra si se solicita
# ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
if porcentaje_total_inicio:
porcentaje = round((total_valor / total_general) * 100, 1)
espacio = "\u00A0"
etiqueta_actual = entidades_formateadas[pos]
resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
if resaltar:
texto_capsula = f"{espacio*1}{etiqueta_actual} {porcentaje}%{espacio*1}"
bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
color_texto = "white"
if orientacion == 'horizontal':
ax.text(
-x_max*0.02, pos, texto_capsula,
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
ax.text(
pos, -x_max*0.02, texto_capsula,
ha='center', va='top',
rotation=90, # <-- Esto rota la cápsula y el texto
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=color_texto,
bbox=bbox_capsula
)
else:
# Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
if orientacion == 'horizontal':
ax.text(
0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
else:
ax.text(
pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
ha='right', va='top',
rotation=90, # <-- Esto rota el porcentaje solo
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color']
)
# Mostrar porcentaje total al lado derecho de la cápsula o del valor total
if porcentaje_total:
porcentaje = round((total_valor / total_general) * 100, 1)
if orientacion == 'horizontal':
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.15 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.18 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
total_valor + desplazamiento, pos, f"{porcentaje}%",
ha='left', va='center',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
else:
if valor_total and quitar_capsula:
desplazamiento = x_max * (0.08 + separar_por_total)
elif valor_total:
desplazamiento = x_max * (0.15 + separar_por_total)
else:
desplazamiento = x_max * (0.03 + separar_por_total)
ax.text(
pos, total_valor + desplazamiento, f"{porcentaje}%",
ha='center', va='bottom',
fontsize=font_config['porcentaje_total']['size'],
fontfamily=font_config['family'],
fontweight=font_config['porcentaje_total']['weight'],
color=font_config['porcentaje_total']['color'])
# Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
if porcentaje_total_inicio and resaltar_etiquetas is not None:
entidades_formateadas = [
"" if e in resaltar_etiquetas else e
for e in entidades_formateadas
]
# Configurar ejes y etiquetas
if orientacion == 'horizontal':
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_yticks(posiciones)
ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'])
ax.invert_yaxis()
ax.set_ylim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_xlim(y_limits)
else:
ax.set_xlim(0, 1.1 * x_max)
ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_xticklabels(),
fontsize=font_config['variable_x']['size'],
fontweight=font_config['variable_x']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_x']['color'])
if grillas:
ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='y', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
else:
x_max = suma_total.max() * 1.15 # <-- AGREGADO
ax.set_xticks(posiciones)
ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
ax.set_xlim(-0.5, len(entidades)-0.5)
if y_limits is not None: # <-- AGREGADO
ax.set_ylim(y_limits)
else:
ax.set_ylim(0, 1.1 * x_max)
ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
plt.setp(ax.get_yticklabels(),
fontsize=font_config['variable_y']['size'],
fontweight=font_config['variable_y']['weight'],
fontfamily=font_config['family'],
color=font_config['variable_y']['color'])
if grillas:
ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
else:
ax.grid(False)
if porcentaje_total_inicio:
ax.tick_params(axis='x', pad=espacio_inicio)
# --- NOMBRE DE EJES ---
if nombre_eje_x is not None:
ax.set_xlabel(
nombre_eje_x,
fontsize=font_config['nombre_eje_x']['size'],
fontweight=font_config['nombre_eje_x']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_x']['color'],
labelpad=18
)
if nombre_eje_y is not None:
ax.set_ylabel(
nombre_eje_y,
fontsize=font_config['nombre_eje_y']['size'],
fontweight=font_config['nombre_eje_y']['weight'],
fontfamily=font_config['family'],
color=font_config['nombre_eje_y']['color'],
labelpad=18
)
# Ajustar rotación de etiquetas especiales según la orientación
if orientacion == 'vertical':
for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
if str(entidad) == "Previo":
label.set_rotation(90) # Cambia a vertical
label.set_ha('center')
elif str(entidad) == "...":
label.set_rotation(0)
label.set_ha('center')
else: # horizontal
for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
if str(entidad) == "...":
label.set_rotation(90)
label.set_va('center')
elif str(entidad) == "Previo":
label.set_rotation(0)
label.set_va('center')
if leyenda:
ncol_leyenda = len(df_wide.columns)
ax.legend(
title=leyenda if isinstance(leyenda, str) else None,
fontsize=font_config['leyenda']['size'],
title_fontsize=font_config['leyenda']['size'],
loc='upper center',
bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
frameon=False,
ncol=ncol_leyenda
)
# Quitar bordes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
plt.tight_layout()
plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
nombre_svg = f"{nombre_df}.svg"
nombre_svg_limpio = f"{nombre_df}_scour.svg"
nombre_svg_svgo = f"{nombre_df}_svgo.svg"
try:
limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
except Exception as e:
print("Error al limpiar con Scour:", e)
try:
limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
except Exception as e:
print("Error al limpiar con SVGO:", e)
plt.show()
# EXPORTAR: barras_verticales_simples
barras_apiladas(df0,
nombre="barras_verticales_simples",
bar_height=0.9,
font='Montserrat',
fontsize_barra=20,
fontsize_valor_total=20,
ordenar_por='etiqueta',
orden='ascendente',
valor_barra=False,
porcentaje_barra=False,
porcentaje_total=True,
orientacion='vertical',
area_min=300
)
Ver carpeta en GitHub